Table of Contents
~~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 GroovyTestCases
s 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 Item
s 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:<Item: 2; name = satchel: value = 10; description: a carrier for school books and pencils;> but was:<Item: 2; name = a different satchel: value = 10;>
...
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 aGroovyTestSuite
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.
- Here we demonstrate how unit testing could have helped.
- We shall create a
PlayerTest
class to go with thePlayer
.
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.
- View source code PlayerTest.groovy (see notes for listing)
- 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)
anddropped
in theItem
class effect thecarrier
property. - method
drop
in thePlayer
class - method
pickUp
in thePlayer
class results in a reference to the current player (this
) being added to theItem
scarrier
property. - methods
pickupItem
anddropItem
in the augmentedGame
.
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.