Testing Vending Machine (Java, Junit)

From LiteratePrograms
Jump to: navigation, search

This program is under development.
Please help to debug it. When debugging
is complete, remove the {{develop}} tag.

This is an article which explains how to use the JUnit test framework to unit test the vending machine class (see Vending Machine (Java)). It also acts as a tutorial for JUnit. Our approach will be to develop the unit tests in the same order that we developed the methods for Vending Machine (Java). In fact we shall use the specification of the behaviour that we put into that article and verified in the main method to derive the tests. The traditional approach of putting executable specifications into a class' main method and the alternative of using unit tests to validate a specification by can thus be compared.

Contents

[edit] Create the Test Fixture

In order to reliably test a Java class, we need to ensure that the class, or an object or set of objects instantiated from that class, will be in a known state. In the original article we created two VendingMchine objects, one for sodas at 75 units and one for candy bars at 60 units. We do the same here, by declaring the vending machines and placing them into a test fixture:

<<declare test fixture>>=
private VendingMachine sodaVendor;
private VendingMachine candyBarVendor;

We also set some prices for these items as compile-time constants:

<<declare test fixture>>=
private static final int SODA_PRICE = 75;
private static final int CANDY_BAR_PRICE = 60;

Before each test case method is executed, JUnit initializes the test fixture:

<<set up test fixture>>=
protected void setUp()
{
    sodaVendor = new VendingMachine(SODA_PRICE);
    candyBarVendor = new VendingMachine(CANDY_BAR_PRICE);
}

And after each test case method is executed, JUnit tears down the test fixture:

<<tear down test fixture>>=
protected void tearDown()
{
    sodaVendor = null;
    candyBarVendor = null;
}

This set up and tear down behaviour, ensures that the test fixture is in a known state when each unit test is executed.

[edit] The Test Cases

[edit] Creating new vending machines

The first test we will write will verify that, after set up, the two VendingMachines have been constructed (are not null) and are the correct type (machine instanceof VendingMachine is true).

<<test the constructor>>=
public void testConstructor()
{
    // Existence test
    assertNotNull("Candy bar vendor exists", 
        candyBarVendor);
    assertNotNull("Soda vendor exists", 
        sodaVendor);
    // Type test
    assertTrue("Candy bar vandor is a vending machine", 
        candyBarVendor instanceof VendingMachine);
    assertTrue("Soda vandor is a vending machine", 
        sodaVendor instanceof VendingMachine);
}

Note that the test consists of four assertions which test the assumptions that we have about the test fixture. If, when the test is executed, the assertion turns out to be false, an exception will be thrown and record by the JUnit test framework as a failure. The string that is the first argument of the assert method will be recorded as part of the failure record.

[edit] Returning price

The price of a vending machine is recorded in a private instance field and is initialized by the constructor. To verify that this has happened we use the accessor method getPrice to verify that the price has been set correctly. We use assertEquals to verify this.

<<test item price is correct>>=
public void testGetItemPrice()
{
    assertEquals("Soda costs " + SODA_PRICE, 
        SODA_PRICE, 
        sodaVendor.getItemPrice());
    assertEquals("Candy bar costs " + CANDY_BAR_PRICE, 
        CANDY_BAR_PRICE, 
        candyBarVendor.getItemPrice());
}

[edit] Testing the current balance

The vending machine accepts coins and vends items. The current state of the machine is captured in the currentBalance field accessed through the getCurrentBalance() method. However, the state of this field needs to be correct at several different stages of the lifecycle of a single transaction. To summarize, these are:

  1. Initially, a newly constructed vending machine will have no coins so its current balance should be zero.
  2. When you add a coin, the balance should equal the value of the coin.
  3. If you add a second coin, the value of current balance should increase by the value of the second coin.
  4. When you vend a coin, the balance should return to zero.

This is quite complex set of behaviours which in the original implementation we tested only superficially. Here we show that with unit testing we can do a much more thorough job. To simplify things, we test only the soda machine, but we'd expect the candy bar machine to work in the same way.

[edit] Initial balance is zero

When a vending machine is constructed, the currentBalance should be zero. A test for this makes use of the getCurrentBalance method and the assertZero:

<<test current balance is initially zero>>=
public void testBalanceIsInitiallyZero()
{
    assertEquals("Balance of soda vending machine is initially zero", 
        0,
        sodaVendor.getCurrentBalance());
}

[edit] Coins add to value of current balance

When a coin is inserted into the vending machine, the current balance should be increased by the value of the coin.

<<test current balance increases by value of coin>>=
public void testBalanceIsIncreasedByCoinValue()
{
    int coin = 10;
    sodaVendor.insertCoin(coin);
    assertEquals("Balance of soda vending machine is equal to " + coin, 
        coin,
        sodaVendor.getCurrentBalance());
}

[edit] Current value accumulates values of coins inserted

The current balance is cumulative. When another coin is inserted into the vending machine, the current balance should be increased by the value of the coin.

<<test current balance accumulates values of coins inserted>>=
public void testBalanceIsCumulative()
{
    int coin = 10;
    sodaVendor.insertCoin(coin);
    sodaVendor.insertCoin(coin);
    assertEquals("Balance of soda vending machine is equal to " + 2 * coin, 
        2 * coin,
        sodaVendor.getCurrentBalance());
}

Note that because the test fixture is torn down and set up again between tests, each test must start from the beginning.

[edit] Vending zeroes balance

Finally, after vending a soda, the current soda machine balance should return to zero. Here we insert coins to the value of the vending price of the soda machine (which we verify with an assertion) then we dispense a soda and check that the balance is now zero.

<<test current balance returns to zero after vending (correct money)>>=
public void testBalanceReturnsToZeroOnVending()
{
    sodaVendor.insertCoin(50);
    sodaVendor.insertCoin(20);
    sodaVendor.insertCoin(5);
    // The price is right!
    assertEquals("We have entered correct money", 
        SODA_PRICE,
        sodaVendor.getCurrentBalance());
    sodaVendor.dispenseItem();
    assertEquals("After vending, the balance of soda vending machine is zero", 
        0,
        sodaVendor.getCurrentBalance());
}

[edit] The complete test of the state of the current balance

Putting all these tests together we have:

<<test the current balance>>=
test current balance is initially zero
test current balance increases by value of coin
test current balance accumulates values of coins inserted
test current balance returns to zero after vending (correct money)

[edit] Inserting Money

The previous test case implicitly used the insertCoins method to test the state of the vending machine's currentBalance property. Let us write a test to validate the insertCoin method itself.

<<test insert one of each coin>>=
public void testInsertOneOfEachCoin() {
   insert one of each coin and validate balance
}


[edit] Define our coinage

The astute reader will have noted that the insertCoin method will take any face value, provided that it's an int. To make things a little more real, we want to define some valid coinage. Here we define all the coins that we have in our currency (this is based on the face values available in the coinage for UK sterling where 100p = 1 UK pound). To make these coins available to all tests, we declare them as static members of the test fixture:

<<declare test fixture>>=
private static int 
    ONE_P = 1, 
    TWO_P = 2, 
    FIVE_P = 5, 
    TEN_P = 10, 
    TWENTY_P = 20, 
    FIFTY_P = 50,
    ONE_POUND = 100, 
    TWO_POUNDS = 200; 

In unit tests we often find it useful to use arrays to generate values which we can feed in to an iteration. Here we define an array of all possible coins:

<<declare test fixture>>=
private int[] coinage = {ONE_P, TWO_P, FIVE_P, TEN_P, TWENTY_P, FIFTY_P, ONE_POUND, TWO_POUNDS};

[edit] Test all coins

A good test of insertCoin would be to step through the coinage array inserting one of each coin into the vending machine and validating the total at the end. We use a utility method int insertCoins(VendingMachine m, int[] coins) (which we describe later) to hide the details:

<<insert one of each coin and validate balance>>=
int valueOfCoins = insertCoins(candyBarVendor, coinage);
assertEquals("Coins to value of " + valueOfCoins + " has been inserted",
    valueOfCoins,
    candyBarVendor.getCurrentBalance());

[edit] Test coins to face value

Let us now create a couple of arrays with value equal to the vend price for the soda and candy bar vendors. Of course there is more than one way to do this, but here's one possible way of making up the price of a soda:

<<test valid money inserted>>=
private int[] correctMoneyForASoda = {FIVE_P, TWENTY_P, FIFTY_P};

Now we put these coins into the machine and validate the result:

<<test valid money inserted>>=
public void testCorrectMoneyInsertedForASoda() {
    int costOfASoda = insertCoins(sodaVendor, correctMoneyForASoda);
    assertEquals("Money inserted for a soda matches vending price",
        SODA_PRICE,
        costOfASoda);
    assertEquals("Balance is also correct",
        SODA_PRICE,
        sodaVendor.getCurrentBalance());
} 

Let's repeat this for the candy bar machine:

<<test valid money inserted>>=
private int[] correctMoneyForACandyBar = {ONE_P, TWO_P, TWO_P, FIVE_P, FIVE_P, FIVE_P, TEN_P, TEN_P, TWENTY_P};
public void testCorrectMoneyInsertedForACandyBar() {
    int costOfACandyBar = insertCoins(candyBarVendor, correctMoneyForACandyBar);
    assertEquals("Money inserted for a candy bar matches vending price",
        CANDY_BAR_PRICE,
        costOfACandyBar);
    assertEquals("Balance is also correct",
        CANDY_BAR_PRICE,
        candyBarVendor.getCurrentBalance());
} 

[edit] Random testing

When you write unit tests, it's easy to produce tests that will pass because they'll never exercise certain unexpected outcomes. The use of random number generator can be useful here as it can create test cases that might occasionally produce an unexpected combination of values and expose a bug. Let's use the java.util.Random class to create some long random sequence of coins to ensure that insertCoin is reliable.

First we add a random number generator to our test fixture:

<<import statements>>=
import java.util.Random;
<<declare test fixture>>=
Random generator = new Random();

Now we create a final test for insert coins that generates a random array of up to 100 coins chosen at random from the coinage array:

<<test balance for a random sequence of coins>>=
public void testBalanceForARandmonSequenceOfCoins()
{
    // Create an array of up to 100 coins
    int[] coins = new int[generator.nextInt(100)];
    // Populate the array with coins 
    for (int i = 0; i < coins.length; i++)
    { // Generate a random coin from the currency collection
      coins[i] = coinage[generator.nextInt(coinage.length)];
    }
    // now feed the candy machine and validate result
    int totalInserted = insertCoins(candyBarVendor, coins);
    assertEquals("Total entered is correct",
        totalInserted,
        candyBarVendor.getCurrentBalance());
}

[edit] Complete test of the insert coin method

Putting these test cases together we get:

<<test insert coin>>=
test insert one of each coin
test valid money inserted
test balance for a random sequence of coins

[edit] Dispensing items

TODO: flesh out documentation for this code segment:

<<test vending>>=
public void testVending()
{
    int valueOfCoins = insertCoins(sodaVendor, new int[]{50, 20, 5});
    assertEquals("Coins equals item price", valueOfCoins, sodaVendor.getItemPrice());
    sodaVendor.dispenseItem();
    assertEquals("Balance should now be 0", 0, sodaVendor.getCurrentBalance());
}

public void testNothingVendedIfInsufficientCoins()
{
    candyBarVendor.insertCoin(CANDY_BAR_PRICE - 10);
    candyBarVendor.dispenseItem();
    assertEquals(CANDY_BAR_PRICE - 10, candyBarVendor.getCurrentBalance());
    candyBarVendor.insertCoin(10);
    assertEquals(CANDY_BAR_PRICE, candyBarVendor.getCurrentBalance());
    candyBarVendor.dispenseItem();
    assertEquals(0, candyBarVendor.getCurrentBalance());
}

[edit] Handling Change

TODO: flesh out this code section.

<<test change handling>>=
public void testVendingLeavesChange()
{
    candyBarVendor.insertCoin(50);
    candyBarVendor.insertCoin(50);
    candyBarVendor.dispenseItem();
    assertEquals(100 - CANDY_BAR_PRICE, candyBarVendor.getCurrentBalance());
}

[edit] Refunding Money

TODO: flesh out documentation for this code segment:

<<test refund>>=
public void testRefundGivesChange()
{
    candyBarVendor.insertCoin(50);
    candyBarVendor.insertCoin(50);
    candyBarVendor.dispenseItem();
    assertEquals(100 - CANDY_BAR_PRICE, candyBarVendor.refundCurrentBalance());
    assertEquals(0, candyBarVendor.getCurrentBalance());
}
	
public void testRefundEmptiesCurrentBalance()
{
    candyBarVendor.insertCoin(50);
    candyBarVendor.insertCoin(50);
    candyBarVendor.dispenseItem();
    candyBarVendor.refundCurrentBalance();
    assertEquals(0, candyBarVendor.getCurrentBalance());
}

[edit] Handling Possible Useage Errors

TODO: flesh out documentation for this code segment:

<<corner cases>>=
public void testZeroCoinRejected()
{
    sodaVendor.insertCoin(50);
    sodaVendor.insertCoin(0);
    assertEquals(50, sodaVendor.getCurrentBalance());
}

public void testNegativeCoinRejected()
{
    sodaVendor.insertCoin(50);
    sodaVendor.insertCoin(-1);
    assertEquals(50, sodaVendor.getCurrentBalance());
}

[edit] The Complete Test Case

TODO: flesh out documentation for this code segment:

<<the test cases>>=
test the constructor
test item price is correct
test the current balance
test insert coin
test vending
test change handling
test refund
corner cases

[edit] Helper Functions

Describe any helper functions here.

<<private helper functions>>=
	private int insertCoins(VendingMachine vm, int[] coins)
	{
	    int coinsInserted = 0;
		for (int coin: coins) 
		{
		    vm.insertCoin(coin);
		    coinsInserted += coin;
		}
		return coinsInserted;
        }


[edit] Running the Unit Tests

In JUnit (version 3.8) a unit test is a method that starts with the word test. The JUnit testing framework uses reflection to call each of these methods in turn, ensuring that each has access to the same test fixture state. Each test typically makes one or more assertions which can be tested to verify the state of the test fixture after a method in the API being tested has been executed. Failure of an assertion is reported whilst success is silently ignored. Thus the developer can concentrate on repairing the problems identified by failing unit tests. It also allows for [test driven development]: an [agile software development] process by which functionality of a class is extended by first defining, by means of a test or tests, what a method should do, before the method is implemented which passes the test.

Unit testing implies that one and only one method or state change is tested in each unit test. A collection of unit tests is called a test case and is usually defined for a single class or possibly a small group of related classes. JUnit also provides for the creation of a collection of test cases, which it calls a test suite, which can be combined using the Composite Pattern, and all executed at the same time. The creation of collections of test cases and test suites allows for the automatic regression testing which gives JUnit its power.

For the vending machine, the complete test case is a class which extends junit.framework.TestCase:

<<TestVendingMachine.java>>=
import statements

/**
 * The test class TestVendingMachine.
 *
 * @author  LiteratePrograms.org
 * @version May 2007.
 */
public class TestVendingMachine extends junit.framework.TestCase
{
    declare test fixture

    /**
     * Default constructor for test class TestVendingMachine
     */
    public TestVendingMachine()
    {
    }

    set up test fixture

    tear down test fixture

    the test cases

    private helper functions

}

To run the test, download the source code into the same project where you have stored the code from Vending Machine (Java). If you are running the code inside a modern Java Interactive Development Environment, you should be able to execute the tests using that tool's standard facility for executing JUnit tests. If you are running from the command line, you'll need a copy of the junit.jar (this code is written to version 3.8.1) which is included in the JUnit distribution for both compilation and running. To compile use:

javac -cp junit.jar;. VendingMachine.java TestVendingMachine.java

To run use:

java -cp junit.jar;. java junit.textui.TestRunner TestVendingMachine

The results will be reported in the console. If you prefer, you can use:

java -cp junit.jar;. java junit.swingui.TestRunner TestVendingMachine

and the results will be displayed inside a nice Swing Graphical User Interface with a prominent red bar for failure and green bar for success.

[edit] Exercises

TODO add exercises which will be test driven development versions of Vending Machine (Java)#Exercises.

Download code
hijacker
hijacker
hijacker
hijacker