User Tools

Site Tools


at-m42:lecture8

~~SLIDESHOW~~

Unit Testing

The slides and notes in this presentation are adapted from Groovy Programming (See Recommended Reading).

An index to the source code for all the examples in this lecture is available.


This lecture explores the use of the JUnit testing framework within the Groovy environment. We use classes from Case Study 3 to illustrate how unit testing can be accomplished using the GroovyTestCase class. Next, we show how several GroovyTestCasess can be combined into a GroovyTestSuite. Finally, we reflect on the role of unit testing in an iterative, incremental approach to software development. Throughout this discussion, we emphasize just how easy it is to benefit from unit testing with Groovy.

Unit Testing

  • Fundamental unit of an object-oriented system is the class
  • Obvious candidate for unit testing is the class.
  • Approach:
    • create an object of the class under test
    • check that selected methods execute as expected
  • Aim is to detect and correct likely failures rather than guarantee 100% test coverage.

Unit testing is a programming activity, and so each unit test involves the internal coding details of the class under test. This is known as white box testing to suggest that we can “look inside” the class to see its internal workings. The alternative is black box testing which, as its name suggests, does not look inside the class. Its purpose is to check the overall effect of a method without any knowledge of how it is internally coded. The use case (functional) tests in Case Study 2 and Case Study 3 are examples of black box testing.

Class to be tested (outline)

class Item {
 
    String toString() { ... }
 
    void pickedUpBy(Player player) { ... }
 
    void dropped() { ... }
 
// ----- properties -----------------------------
    def description = ''
    def id
    def value
    def carrier
}

The full Item class is reproduced in the notes.


1 | Class to be tested (at-m42/Examples/lecture08/Item.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/Item.groovy

To illustrate the idea we will use the Item class from Case Study 2.

Naïve Test Case

1 | Example 1: Testing with println (at-m42/Examples/lecture08/example1.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/example1.groovy

Here we simply create a couple of items and print them. This demonstrates that the toString method has been overridden and behaves as expected.

Test output

Item: 2001; name = Cloth of Gold: value = 25;
Item: 2002; name = Shiny pebble: value = 0; description: A shiny pebble, found on the beach. 
Has sentimental value only!;

The output is indeed as expected (note the use of the triple quoted string to enable a multi-line description to be given). But this manner of testing is rather tedious: it produces a lot of output which presumably has to be read by someone.

Testing with assertions

1 | Example 2: Testing with assertions (at-m42/Examples/lecture08/example2.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/example2.groovy

Here we successfully validate the expectations of the code, but the run is now silent, we get no feedback and we would have to remember to run the test code whenever we modify the Item class.

The GroovyTestCase and Junit TestCase Classes

1 | Example 3: Testing with JUnit TestCase (at-m42/Examples/lecture08/ItemTest.groovy)
// Testing with jUnit
import groovy.util.GroovyTestCase
 
class ItemTest extends GroovyTestCase {
 
  /**
   * test that expected String is returned from toString
   */
  def void testToString() {
      def item1 = new Item(id : 2001, name : 'Cloth of Gold', value : 25)
      def expected = 'Item: 2001; name = Cloth of Gold: value = 25;'
 
      assertToString(item1, expected)
  }
 
  // ... continued on next slide
}

View complete source code: ItemTest.groovy

Example 3 Continued

1 | Example 3: Testing with JUnit TestCase ... continued (at-m42/Examples/lecture08/ItemTest.groovy)
// Testing with jUnit
import groovy.util.GroovyTestCase
 
class ItemTest extends GroovyTestCase {
 
 // ... continued from previous slide
 
  /**
   * test that toString has a description, if item has
   */
  def void testToStringHasDescription() {
      def item = new Item(id : 2002, name : 'Shiny pebble', value: 0, 
                          description : """A shiny pebble, found on the beach. 
Has sentimental value only!""")
 
      def result = item.toString()
      assertTrue(result.startsWith('Item: 2002; name = Shiny pebble: value = 0; description:'))
      assertTrue(result.endsWith('sentimental value only!;'))
  }
}

View complete source code: ItemTest.groovy


GroovyTestCase executes the tests automatically. Every method that starts with test is assumed to be a test. The assert methods AssertToString (used on line 13 on the previous slide) and AssertTrue (lines 17 and 18 on this slide) document our expectations.

Note that we can have as many assertions as we want in a test case, but it is best practice to test only one thing at a time. I used startsWith and endsWith here (lines 17 and 18) because the character used for the end of line is somewhat platform dependent and I didn't want to make assumptions of how the newline character in the description will be rendered in the description property.

When the test case runs, the output is:

..
Time: 0.002

OK (2 tests)

Note that although the tests are essentially the same as we had with the assertions version, we now get some feedback: each dot represents the execution of a test and success is indicated with “OK” when all tests are run. If there are errors, the test result is more descriptive and it will list the errors at the end of the run. However, it will not stop at the first error, as a failed assertion using assert would. Rather it will continue until all tests have been run. Thus you always run a complete set of tests each time.

Testing the Game Class

1 | The Game Class (at-m42/Examples/lecture08/Game.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/Game.groovy

TestCase for the Game Class

1 | Example 4: The GameTest Class (at-m42/Examples/lecture08/GameTest.groovy)
// TestCase for the Game class
 
import groovy.util.GroovyTestCase
 
class GameTest extends GroovyTestCase {
 
	/**
	 * Set up the fixture
	 */
	void setUp() {
		game = new Game(name : 'School')
 
		book = new Item(id : 1, name : 'book', value : 5, 
			description : 'a maths text book')	
		satchel = new Item (id : 2, name : 'satchel', value : 10,
			description : 'a carrier for school books and pencils')
	}
 
    // continued on next slide
 
}

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


The method setUp establishes the environment (context) in which each test method operates. The test environment is known as a test fixture and it must be initialized each time a test method is executed. This ensures that there is no difference between tests and that they can be run in any order. Groovy arranges for setUp to be executed before the execution of each test method.

In our GameTest class, the test fixture is a Game object referenced by game, and two Item objects referenced by book and satchel respectively.

testAddItem_1

1 | Example 4: The GameTest Class (continued) (at-m42/Examples/lecture08/GameTest.groovy)
class GameTest extends GroovyTestCase {
 
	// continued from previous slide
 
	/**
	 * Test that the addition of an item to the game results in one more
	 * item in the game.
	 */
	 void testAddItem_1() {
	 	def pre = game.inventory.size()
	 	game.addItem(book)
	 	def post = game.inventory.size()
 
	 	assertTrue('one less item than expected', post == pre + 1)
	 }
 
	// continued on next slide
}

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


Notice that we use a numbering scheme with a suitable comment for a method with several tests. However, the use of more meaningful method names, such as testAddItemWithDifferentId, is a popular (self documenting) alternative.

In each test method, we assert that something is true. if it is not, the the test fails and the failure is reported with a suitable message. For example, in the test method shown on this slide, we assert that on completion of the method, there should be one more Item in the Game with:

assertTrue('one less item than expected', post == pre + 1)

If the condition post == pre + 1 evaluates to true, then the assertion is true. Otherwise the assertion is false and a failure is reported with the text:

one less item than expected

incorporated into it to help us identify the nature of the problem.

testAddItem_2

1 | Example 4: The GameTest Class (continued) (at-m42/Examples/lecture08/GameTest.groovy)
class GameTest extends GroovyTestCase {
 
  // ...
 
	/**
	 * Test that the addition of two items with different ids to an
	 * empty game results in a game with two items in its inventory
	 */
	 void testAddItem_2() {
	 	game.addItem(book)
	 	game.addItem(satchel)
	 	def expected = 2
	 	def actual = game.inventory.size()
 
	 	assertTrue('unexpected number of items', expected == actual)
	 }
 
  // continued on next slide
}

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


In testAddItem_2, we assert that the addition of two Items with different ids into an empty Game should result in a Game with two Item objects in it.

The properties

Needed for the fixture.

1 | Example 4: The GameTest Class (concluded) (at-m42/Examples/lecture08/GameTest.groovy)
class GameTest extends GroovyTestCase {
 
  // ...
 
// ----- properties --------------------------
 
	def game
 
	def book
	def satchel
}

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


The elements of the test fixture game, book and satchel are defined as properties of the GameTest class. This makes them available to all test methods.

Results of executing GameTest

Output:

..
Time: 0.024

OK (2 tests)

This shows us that two tests have passed. Although they appear rather simple this tests give us confidence that the Game class is behaving as planned. There is no need to construct elaborate unit tests. In fact, it is normally much better to have several tests, each of which tests just one logical path though the method under test. They are not a burden because they are automatically compiled, executed and checked. As we add more tests, we gain more an more confidence in our code.

Pose a question

  • Unit testing is all about using our experience as programmers to detect and correct possible failures in our code.
  • To illustrate, let's ask the question: “What happens if we attempt to add an item with the same id as one already in the game?”

Add another unit test

1 | Example 5: Add another test (at-m42/Examples/lecture08/GameTest.groovy)
    /**
     * Set up the fixture
     */
    void setUp() {
        // ...
         item3 = new Item (id : 2, name : 'a different satchel', value : 10)
    }
 
    // ...
 
     /**
      * Test that the addition of an Item with the same id as one
      * already present in the Game results in no change in the number
      * of items in the inventory
      */
     void testAddItem_3() {
         game.addItem(book)
         game.addItem(satchel)
         def pre = game.inventory.size()
         game.addItem(item3)
         def post = game.inventory.size()
 
         assertTrue('one more item than expected', post == pre)
     }      
   // ...

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


Perhaps we are not sure but suspect that an Item with the same id as one already in the Game is not added: a suitable test is the one shown in the slide.

Note that although for space reasons it isn't shown, you need to add def item3 to the properties of GameTest!

Result now

...
Time: 0.02

OK (3 tests)

Another question

  • Is the Item the original one or a new one?

Another test

1 | Example 5: Another test (at-m42/Examples/lecture08/GameTest.groovy)
//class GameTest
 
     /**
      * Test that the addition of an Item with the same id as one
      * already present in the Game results in no change in items in the inventory
      */
     void testAddItem_4() {
         game.addItem(satchel)
         game.addItem(item3)
         def expected = satchel.toString()
         def actual = game.inventory[2]
 
         assertToString(actual, expected)
     }        
   // ...

Download complete source code: http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/GameTest.groovy.


In the previous example we created a third item with the same id as the satchel and attempted to add it to the game. However, just because this object has the same id and property values it is a different object. We want the original item to be still in the Game. So this test verifies that.

The result (reformatted to fit slide)

<html> <pre> ….F Time: 0.003 There was 1 failure: 1) testAddItem_4(GameTest)junit.framework.AssertionFailedError: toString() on value: Item: 2; name = a different satchel: value = 10; expected:&lt;Item: 2; name = satchel: value = 10; description: a carrier for school books and pencils;&gt; but was:&lt;Item: 2; name = a different satchel: value = 10;&gt;

...

FAILURES!!! Tests run: 4, Failures: 1, Errors: 0 </pre> </html>

Now the test report informs us that the fourth test method failed (hence the four dots and the F) It goes on to give more information about the failure. Although it does not concern us here, note that if an unexpected exception occurs, an error, not a failure is reported.

Correct error

l|Correction to code
// class Game
void addItem(Item item) {
  if (! inventory.containsKey(item.id) ) {
    inventory[item.id] = item
  }
}

Verify this change passes the test!


Having established that there is a problem with the addItem method in the Game classm re now recode it as shown in the slide and re-runGameTest to give a successful test result.

Happily all four tests now pass and as a result we have fixed a bug and have gained more confidence in our code. Notice that we made the minimum changed necessary to pass the fourth test and that none of the previous tests was invalidated.

The GroovyTestSuite and Junit TestSuite Classes

  • One test class per class in an application
  • Want to execute all tests at once
  • GroovyTestCase makes it eay to write a JUnit test class.
  • GroovyTestSuite makes it easy to combine test cases into a test suite.
  • All GroovyTestCases in a GroovyTestSuite execute together.
  • Easy to run a full range of tests.

GroovyTestSuite runAllTests.groovy

l|Example 6: A Groovy test suite (at-m42/Examples/lecture08/runAllTests.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/runAllTests.groovy

The AllTests class has a static method, suite. It returns a GroovyTestSuite, referenced by allTests, to which a Class object for each GroovyTestCase has been added. Note that Test is an interface implemented by GroovyTestSuite. It just ensures that a GroovyTestSuite can be run.

On execution of the script, there is a call to the static run method of the TestRunner class. The actual parameter to this method call is the trest object returned by the suite method of the class AllTests. The run method automatically executes each GroovyTestCase in the GroovyTestSuite. In our case they are the ItemTest and the GameTest classes developed previously. The details of how this is accomplished need not concern us here, but interested readers should consult the Junit web site (see http://www.junit.org) for more information.

Just as with a GroovyTestCase, we compile and executed runAllTests as normal to get a test report. As before all five tests (one from ItemTest and four from GameTest) pass. Noet that previously the TestRunner was executed “under the covers”. Here, we find it convenient to make its presence explicit. Interested readers may like to consult the Groovy website (http://grovvy.codehaus.org) for alternatives.

A further minor change

l
    // class Game
 
    Boolean addItem(Item item) {
        if ( ! inventory.containsKey(item.id) ) {
            inventory[item.id] = item
            return true
        } else {
            return false
        }
    }

To appreciate just how useful unit testing is, let's return to the addItem method in the Game class. We decide that we'd like it to report on success of failure of adding an Item. Therefore we record the method so that it returns a Boolean status value as shown here.

Test for success

l|Example 7: New tests added to TestSuite
     /**
      * Test that successfully adding an Item to the Game
      * is detected
      */
     void testAddItem_5() {
         def success = game.addItem(satchel)
 
         assertTrue('addition should succeed', success)
     }      

Complete source code: GameTest.groovy


We add a test for success …

Expect failure

l|Example 7: New tests added to TestSuite (continued)
      /**
      * Test that unsuccessfully attempting to add Item with the same 
      * id as one already present in the Game is detected.
      */
     void testAddItem_6() {
         game.addItem(satchel)
         def success = game.addItem(item3)
 
         assertFalse('addition should fail', success)
     }  

Complete source code: GameTest.groovy


… and one for failure.

Notice that assertFalse returns true if the condition evaluates to false. We find it more convenient than the equivalent

assertTrue('no addition expected', success == false)

Because all of the previous tests have passed, we are reasonably confident that any changes made have not had a detrimental effect on the rest of our code. This has been achieved with minimum effort on our part. This is one of the reason why unit testing is such a powerful weapon in our armoury.

The Role of Unit Testing

  • Unit testing is an integral part of modern iterative, incremental approach to software development.
  • In Iteration II of Case Study 3 we developed a Player class.
  • Here we demonstrate how unit testing could have helped.
  • We shall create a PlayerTest class to go with the Player.

The Player Class

1 | The Player class (at-m42/Examples/lecture08/Player.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/Player.groovy

Notes ….

The PlayerTest class

Similar to GameTest but too big to show on a slide.


1 | The PlayerTest class (at-m42/Examples/lecture08/PlayerTest.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/PlayerTest.groovy

Correction to PlayerClass to pass all tests

    def pickUp(item) {
        if (! inventory.containsKey(item.id) ) {
        	inventory[item.id] = item
        	item.pickedUpBy(this)
        	return true
        } else {
        	return false
        }
    }
  • Exercise: add tests for success/failure of pickUp method (see earlier example).

Add new test case to TestSuite

1 | The extended test suite (at-m42/Examples/lecture08/runAllTests2.groovy)
extern> http://www.cpjobling.org.uk/~eechris/at-m42/Examples/lecture08/runAllTests2.groovy

Extending GameTest

In Iteration II of Case Study 3 we extended the Game class to allow players to be registered with the game. We should test these methods (see notes and Lab exercises)


to GameTest
// class GameTest
 
/**
 * Set up the fixture
 */
void setUp() {
 
    // ...
    player = new Player(id : 7, nickname : 'james', email : 'jb@sis.gov.uk')
}
 
// ...
 
/**
 * Test that registering a Player with an empty Game results 
 * in one more Player in the Game.
 */
void testRegisterPlayer_1() {
    def pre = game.players.size()
    game.registerPlayer(player)
    def post = game.players.size()
 
    assertTrue('one less player than expected', post = pre + 1)
}
 
// ...
 
def player
 
// ...

Additional Tests

We should test additional methods that Iteration II introduced. For example, the following have not been tested:

  • methods pickedUpBy(player) and dropped in the Item class effect the carrier property.
  • method drop in the Player class
  • method pickUp in the Player class results in a reference to the current player (this) being added to the Items carrier property.
  • methods pickupItem and dropItem in the augmented Game.

Case Study

Case Study 4 further illustrates the use of Class Inheritance and unit testing while continuing the development of the adventure game application. You should read through the case study and examine the source code provided in preparation for the Mini Project.

Summary of this Lecture

Lab Exercises

at-m42/lecture8.txt · Last modified: 2011/01/14 12:45 by 127.0.0.1