CPSC 329 Software Development Fall 2017

CPSC 329 Lab 7: JUnit

Testing is an important part of establishing the correctness and robustness of a program. The purpose of this lab is to give you some practice with using JUnit, a testing framework which automates much of the work of running unit tests.

In particular, you will be testing some classes which are useful when dealing with money in a variety of currencies. These classes have been adapted from an example in the JUnit documentation.

Objectives

Successful completion of this lab means that you:

Collaboration

Work in pairs (and one group of three) to complete this lab. Only one person needs to carry out the steps in the lab but everyone in the group should make sure they understand what is going on. There is no limit on collaboration with others, but you need to make sure that you understand for yourself how (and why) to do things.

Due Date and Deliverables

due Tue Oct 24 at the start of class

One handin per group.

To hand in the lab, create a handin tag as directed.


Setup

Create a project for this lab:

You have been provided with three files: SingleCurrency, MoneyBag, and Money. SingleCurrency represents an amount of money in a single currency (e.g. 10 HRK), while MoneyBag represents an amount of money which is in more than one currency (e.g. 10 HRK and 20 USD). Money is an interface which is implemented by both SingleCurrency and MoneyBag - its purpose is to make it possible to write a program which can simply work with Money and not have to worry about whether the money is in a single currency or not.

Money defines the one thing that can be done with money: you can add two amounts of money. (Subtract by adding a negative amount.) add can take anything that implements the Money interface as a parameter, including both SingleCurrency and MoneyBag. Note that add returns a new object containing the new amount of money - SingleCurrency and MoneyBag are immutable.

Money has two additional methods which are used in the implementation of add, and which handle adding a particular kind of money (SingleCurrency or MoneyBag). Note that these are not public - they are meant to be used only by SingleCurrency and MoneyBag themselves. (If you are wondering why they are there, this is an example of something called double dispatch - the end result is that we can avoid writing an 'if' statement involving instanceof to handle the different kinds of Money that can be added in the body of add. This keeps each method simpler.)

SingleCurrency provides a constructor, getters for the currency and amount, implementations for each of the methods in Money, and an equals method. equals is a standard method that all classes have; the default behavior is to treat two objects as equivalent if and only if they are the same object (i.e. in the same memory location). Since that's not what we want here, it is overridden.

MoneyBag provides three constructors (two private ones used only by the MoneyBag class and a package-access one which is intended only for use by SingleCurrency), implementations for each of the methods in Money, an equals method, and several private helper methods. See the comments in each class for more details about the behavior of each method.


Unit Testing and Test Cases

Unit testing is a methodology which focuses on testing individual units of functionality (such as individual methods). The idea is that small chunks are easy to work with, and the whole isn't going to work if the parts don't work.

Thoroughly testing a method requires defining one or more test cases. Continuing the idea that small chunks are easier to work with, each test case should cover one aspect of the method's functionality - in short, a test case should have a single reason to fail.

Formally defining a test case for a method requires four elements:

Let's consider SingleCurrency's equals method. Seven test cases were identified in class:

test namestarting stateinputexpected output
same currency, same amount (non-zero) SingleCurrency object representing 10 USD (a different) SingleCurrency object representing 10 USD true
same currency, different amount (non-zero) SingleCurrency object representing 10 USD SingleCurrency object representing 20 USD false
different currency, same amount (non-zero) SingleCurrency object representing 10 USD SingleCurrency object representing 10 HRK false
both amounts 0 (different currency) SingleCurrency object representing 0 USD SingleCurrency object representing 0 HRK true
first amount is 0 (obj has non-zero amount) SingleCurrency object representing 0 USD SingleCurrency object representing 20 USD false
obj is not a SingleCurrency object SingleCurrency object representing 10 USD "hello" false
obj in null SingleCurrency object representing 10 USD null false

Writing and Running Tests With JUnit

JUnit is a testing framework which automates much of the work of running tests, making it easy and convenient (and fun!) to test your code each time a change is made.

Creating a JUnit Test Class

In JUnit, a separate method is written for each test case being implemented. A bunch of test cases - typically ones for methods belonging to a since class - are grouped into a test class.

Start by creating a test class for SingleCurrency. Eclipse will auto-generate some of the test class code for you:

You should now have a class SingleCurrencyTest with three methods: setUp(), tearDown(), and test().

Fixtures

Setting up the starting state for a test can involve some effort, and if the same starting state is used for multiple tests (such as the 10 USD SingleCurrency object above), you can save some work by creating a fixture.

A fixture is implemented with an instance variable - as with any class, the instance variables of that class are available to all methods of the class. However, instead of initializing a fixture in a constructor, fixtures are initialized by the setUp() method. The JUnit system automatically calls setUp() before each test method is run so each test method gets a fresh copy unaffected by anything a previously-run test method might have done.

Create a fixture for the 10 USD SingleCurrency object:

Several other objects could be fixtures since they are used in more than one test case:

(In this case, the setup of the various objects is pretty easy and not a lot is gained by creating fixtures. But do it anyway to get the practice.)

Implementing Test Cases

We are now ready to implement the seven test cases identified above.

Start with the first test case, same currency and amount:

assertTrue is a JUnit routine - it takes a boolean as a parameter, and causes the test to fail if the parameter is not true. assertFalse causes the test to fail if the parameter is not false.

Running Tests

Once you have some tests implemented, it's time to run them!

You'll see that the Package Explorer is replaced by a view that shows you the results of the tests. If all is well, you'll see little green checks by each test class and test method run. Blue or red Xs indicate problems.

Normally you would work on fixing any bugs that caused tests to fail before writing more tests, but continue on for now.

Objects as Output

Let's move on to testing SingleCurrency's addSingle method. The following test cases cover its functionality:

test namestarting stateinputexpected output
add to zero amount SingleCurrency object representing 0 USD SingleCurrency object representing 15 HRK SingleCurrency object representing 15 HRK
add zero amount SingleCurrency object representing 15 HRK SingleCurrency object representing 0 USD SingleCurrency object representing 15 HRK
add same currency (non-zero amounts) SingleCurrency object representing 10 USD SingleCurrency object representing 15 USD SingleCurrency object representing 25 USD
add different currency (non-zero amounts) SingleCurrency object representing 10 USD SingleCurrency object representing 15 HRK MoneyBag object representing 10 USD, 15 HRK

assertEquals uses the equals method to compare the two objects, and this illustrates an important point - it is not always possible to test a single method in isolation because setting up the starting state, setting up the input, setting up the expected output, and/or comparing the actual output with the expected result may use non-trivial methods of the class under test or another class of the program. In this case we can remain close to our goal of "one reason to fail" because we've already written test cases for equals - if those tests pass, a failure of the "same currency" test case is the fault of addSingle and not equals.

Of course, this means that you are now using two methods of MoneyBag - createMoney and equals - which could cause the test to fail even if addSingle is correct, and we haven't yet written test cases for those. That should be done in order to maintain "one reason to fail", but you don't have to do it now.

Finally,

Handling Exceptions

Sometimes the behavior of a method includes throwing an exception in certain cases. Violated preconditions aren't normally something that would need to be tested since a method's behavior isn't guaranteed if preconditions are violated (so there's not really any correct behavior in that case), but we'll add a test case involving preconditions here in order to illustrate how to deal with testing exception-throwing behavior.

If addSingle throws the expected exception, control will immediately transfer to the catch block. Since there's nothing there and nothing else in the body of the method, the test method exits and the test is deemed successful. If addSingle fails to throw the exception, control will not transfer to the catch block and the fail line will be executed, causing the test to fail. If addSingle throws some other kind of exception, it will not be caught and the test will fail with an uncaught exception.

Finally,

Test Suites

If all of your tests are implemented in a single test class, it is easy to run them all. If there are multiple test classes, you can run all of the tests in one step by creating a test suite.

But first we need a second test class...

The following test cases cover MoneyBag's addSingle method:

test namestarting stateinputexpected output
add zero amount MoneyBag object representing 10 USD, 15 HRK SingleCurrency object representing 0 HRK MoneyBag object representing 10 USD, 15 HRK
add existing currency MoneyBag object representing 10 USD, 15 HRK SingleCurrency object representing 15 USD MoneyBag object representing 25 USD, 15 HRK
add new currency MoneyBag object representing 10 USD, 15 HRK SingleCurrency object representing 15 EUR MoneyBag object representing 10 USD, 15 HRK, 15 EUR
canceling out one currency of two MoneyBag object representing 10 USD, 15 HRK SingleCurrency object representing -10 USD SingleCurrency object representing 15 HRK
canceling out one currency of many MoneyBag object representing 10 USD, 15 HRK, 20 EUR SingleCurrency object representing -10 USD MoneyBag object representing 15 HRK, 20 EUR

Now, create a test suite for running all of the tests from both classes:

If you add additional test methods to existing test classes, running the whole test suite will automatically include the new test methods. If you add new test classes, you'll need to right-click on AllTests in the Package Explorer and choose Recreate Test Suite... This will update the test suite to include the new test classes.


If You Have Time

If you have time, tackle the following (in order):

Commit to the repository after each set of test cases implemented or bug fixed.


Handin


Valid HTML 4.01!