2. Unit Testing With JUnit

Author:Nick Efford
Contact:N.D.Efford@leeds.ac.uk
Status:Final
Revised:2017-09-15

The aim of this worksheet is to help you learn how the JUnit framework can be used for unit testing of code, specifically from within Intellij IDEA. If you have further questions about JUnit after doing this work, you may find that the JUnit FAQ answers them.

Note

We’ve flagged this worksheet as ‘essential’, but learning how to write JUnit tests is the important thing. Learning how IntelliJ supports unit testing is much less important.

If you’re not a fan of IntelliJ, feel free to ignore those aspects of the worksheet and focus instead on the use of JUnit itself. The main thing is to make sure that you can write and run JUnit tests successfully, irrespective of the environment you use for that.

2.1. Getting Started

  1. Start IntelliJ and create a new Java project called JUnit. Download Time.java and copy it into the src directory of the project. When the Time class appears in the Project tool window, double-click on it to open it in the editor.

    Examine the code and you will see that the package declaration at the top of the file is underlined with a red wavy line, signifying an error. Notice also the thin red marker appearing at the right margin. This is a link that you can hover over to see details of the error or click on to quickly navigate to the line from elsewhere in the file.

    The problem here is that Java expects classes to be organised in a directory tree that matches their package declarations. In this case, it means that the Time class should be in the directory src/comp2931/time underneath the project.

    Fortunately, IntelliJ can help here. If you click on the package name, you should see a red ‘light bulb’ icon appear, signifying that a fix is available. Click on this and choose the first option, ‘Move to package comp2931.time’. This will resolve the problem.

    ../_images/packagefix.png
  2. Some further set-up is necessary before you can begin testing the Time class using JUnit. Right-click on the JUnit project directory in the Project tool window and choose New ‣ Directory. Enter tests as the directory name and click OK.

    Then right-click on tests and choose Mark Directory as ‣ Test Sources Root. This tells IntelliJ where tests should be located in the project.

    Note

    Although you can put your tests into the same directory as the code they are testing if you want, it is cleaner to keep the two separated.

2.2. Creating a Test Class

In order to test the behaviour of the Time class using JUnit, you need to create a separate class that contains all of the tests.

  1. Click on the first line of Time.java. When the yellow ‘light bulb’ icon appears, click on it and choose the ‘Create Test’ option.

    In the resulting dialog, choose ‘JUnit4’ as the testing library. IntelliJ will report that the library is not found.

    ../_images/testclass.png

    Click the Fix button and choose the ‘Copy JUnit4 library files’ option, then click OK. Make sure sure that the test class name is set to TimeTest and that the destination package is comp2931.time. Then click OK to create the test class.

    IntelliJ will create a new class TimeTest and open the file TimeTest.java for you in the editor. Change the default doc comment at the start of the class definition to something more appropriate, such as ‘Unit tests for the Time class’.

  2. Examine the layout of the project in the Project tool window. Notice how the TimeTest class has been given a package declaration that matches the one given to Time. Notice also that the class has been put under the tests directory rather than under src. Finally, notice that a new directory called lib has been created, containing two JAR files. These are the libraries of code needed to support testing using JUnit.

    ../_images/projlayout.png

2.3. Writing & Running Tests

  1. Add the following import statement to TimeTest.java:

    import static org.hamcrest.CoreMatchers.*;
    

    You will need this to write assertions for the tests using the more readable ‘Hamcrest’ syntax.

  2. Now put the cursor inside the definition of TimeTest and either right-click and choose Generate… or press Alt+Insert. From the resulting pop-up menu, choose Test Method ‣ JUnit4. Modify the generated test so that it looks like this:

    @Test
    public void hours() {
      Time midnight = new Time(0, 0, 0);
      Time noon = new Time(12, 0, 0);
      Time elevenPm = new Time(23, 0, 0);
    
      assertThat(midnight.getHours(), is(0));
      assertThat(noon.getHours(), is(12));
      assertThat(elevenPm.getHours(), is(23));
    }
    
  3. To run this test, a suitable run configuration is needed. You could create this manually, but there is an easier way. Click on the green icon that appears in the left margin at the start of the class definition and choose the option to ‘Run TimeTest’.

    A run configuration will be created and the tests in TimeTest will execute. Results will appear in the Run window at the bottom of the UI.

    ../_images/greenbar.png

    Notice how the Run window works differently when running JUnit tests. The tests are listed in a panel on the left and details of failing tests appear in the wider panel on the right. In this case, there are no failures - indicated by the green bar at the top of the Run window.

Note

Depending on the versions of Java and IntelliJ that you are using, and where you are running them, you might see a warning about ‘JavaLaunchHelper’ displayed in the Run window. You can safely ignore this. (It’s a bug that will be fixed in a forthcoming release of the JDK.)

2.4. Testing For Exceptions

  1. So far, you have tested only that the hours component of a Time object is initialised correctly when a legal value is supplied to the constructor. What if an illegal value is supplied instead? The Javadoc comments for Time indicate that such values should trigger an IllegalArgumentException, so you need to verify that this is what actually happens.

    Add the following to TimeTest:

    @Test(expected=IllegalArgumentException.class)
    public void hoursTooLow() {
      new Time(-1, 0, 0);
    }
    

    Then add a comparable test case for an hours value that is too high. Do this in a new method, not in hoursTooLow. You should now have three tests in TimeTest.

    Run the tests again and you should see a red bar, because one of the three tests will have failed. (If you don’t see this, try using a different hours value. Remember the importance of testing at the boundaries where behaviour is supposed to change.)

  2. Check the Run window to find out which test failed. Use this information to find the problem in Time.java. Fix the problem, then check that you have succeeded by rerunning the tests; if the bar is green again, you’ve done it!

2.5. Alternative Assertions

  1. Study the equals method in the Time class. This should return: true when a Time object is compared with itself; true when it is compared with a different Time object having the same internal state; false when it is compared with a different Time object having a different internal state; false when it is compared with some other type of object (regardless of whether that other object resembles the Time object in some way).

  2. Now write the following test for equals in TimeTest:

    @Test
    public void equality() {
      Time noon = new Time(12, 0, 0);
    
      assertTrue(noon.equals(noon));
      assertTrue(noon.equals(new Time(12, 0, 0)));
      assertFalse(noon.equals(new Time(13, 0, 0)));
      assertFalse(noon.equals(new Time(12, 1, 0)));
      assertFalse(noon.equals(new Time(12, 0, 1)));
      assertFalse(noon.equals("12:00:00"));
    }
    

    Can you see the correspondence between this code and the requirements for the behaviour of the equals method stated above? [1]

    Notice that different types of assertion are being used here: assertTrue and assertFalse, rather than assertThat. In cases where the method under test returns a boolean value, this form is a little simpler. You could still use assertThat if you wished:

    assertThat(noon.equals(noon), is(true));
    assertThat(noon.equals("12:00:00"), is(false));
    

    You could even use this:

    assertThat(noon, equalTo(noon));
    assertThat(noon, not(equalTo("12:00:00")));
    

    equalTo and not are examples of Hamcrest matchers, just like the is used in earlier assertions. The reason that equalTo works here is that under the hood it uses an object’s equals method to test for equality. (However, the earlier examples are probably better, because they call the method being tested explicitly.)

  3. Rerun the tests to check that you still have a green bar.

  4. There are other kinds of assertion and a whole range of different matchers that can be used with assertThat. Have a look at the JUnit assertion examples and the Hamcrest Tutorial for more information.

2.6. Test Fixtures

Did you notice some duplication in the test cases implemented so far?

Both hours and equality create a Time object to represent noon. If you find the same object being used multiple times, then you can save yourself some typing and simplify your tests by making that object part of a test fixture: a fixed context that is generated automatically for you at the start of each test.

  1. Define noon as a private field at the start of the TimeTest class:

    private Time noon;
    

    Then remove the lines defining local variable noon from the hours and equality methods.

  2. Now add the following to TimeTest, after the field definition and before any of the test cases:

    @Before
    public void setUp() {
      noon = new Time(12, 0, 0);
    }
    

    The @Before annotation tells JUnit that the setUp method creates a test fixture. The noon field will be guaranteed to have this value before each test executes.

  3. Run the tests again. There should be no change in the output from JUnit.

2.7. More Testing

  1. Add tests for the inSeconds and plus methods of Time. Make assertions using assertThat where possible and remember to check that plus generates an IllegalArgumentException when expected to do so.

    Run your tests and, if they fail, fix the corresponding problems in the Time class.

  2. Add a test called timeToString that tests the toString method of the Time class. Then add a test called stringToTime to test creation of Time objects from strings such as "12:30:06" and "17:45".

    Run your tests and, if they fail, fix the corresponding problems in the Time class.

  3. If you find that you have reused the same Time objects across multiple tests, refactor TimeTest so that these objects are created in your test fixture. Make sure that your tests still behave correctly afterwards!


[1]This example tests all aspects of equals behaviour via a single test method. An alternative (and perhaps better) approach would be to have four smaller tests, one for each aspect of the behaviour.