======================= 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. .. _JUnit: http://junit.org/ .. _JUnit FAQ: http://junit.org/junit4/faq.html Getting Started =============== #. Start IntelliJ and create a new Java project called ``JUnit``. Download :download:`Time.java` and copy it into the :file:`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 :file:`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. .. figure:: packagefix.png :align: center #. 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 :menuselection:`New --> Directory`. Enter ``tests`` as the directory name and click :guilabel:`OK`. Then right-click on ``tests`` and choose :menuselection:`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. 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. #. 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. .. figure:: testclass.png :scale: 80 % :align: center Click the :guilabel:`Fix` button and choose the 'Copy JUnit4 library files' option, then click :guilabel:`OK`. Make sure sure that the test class name is set to ``TimeTest`` and that the destination package is ``comp2931.time``. Then click :guilabel:`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'. #. 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 :file:`tests` directory rather than under :file:`src`. Finally, notice that a new directory called :file:`lib` has been created, containing two **JAR files**. These are the libraries of code needed to support testing using JUnit. .. figure:: projlayout.png :scale: 80 % :align: center Writing & Running Tests ======================= #. Add the following import statement to :file:`TimeTest.java`: .. code-block:: java import static org.hamcrest.CoreMatchers.*; You will need this to write assertions for the tests using the more readable 'Hamcrest' syntax. #. Now put the cursor inside the definition of ``TimeTest`` and either right-click and choose :menuselection:`Generate...` or press :kbd:`Alt+Insert`. From the resulting pop-up menu, choose :menuselection:`Test Method --> JUnit4`. Modify the generated test so that it looks like this: .. code-block:: java @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)); } #. 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. .. figure:: greenbar.png :scale: 80 % :align: center 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.) Testing For Exceptions ====================== #. 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``: .. code-block:: java @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.) #. 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! Alternative Assertions ====================== #. 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). #. Now write the following test for ``equals`` in ``TimeTest``: .. code-block:: java @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? [#]_ 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: .. code-block:: java assertThat(noon.equals(noon), is(true)); assertThat(noon.equals("12:00:00"), is(false)); You could even use this: .. code-block:: java 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*.) #. Rerun the tests to check that you still have a green bar. #. 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. .. _JUnit assertion examples: https://github.com/junit-team/junit/wiki/Assertions .. _Hamcrest Tutorial: https://code.google.com/archive/p/hamcrest/wikis/Tutorial.wiki 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. #. Define ``noon`` as a private field at the start of the ``TimeTest`` class: .. code-block:: java private Time noon; Then remove the lines defining local variable ``noon`` from the ``hours`` and ``equality`` methods. #. Now add the following to ``TimeTest``, after the field definition and before any of the test cases: .. code-block:: java @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. #. Run the tests again. There should be no change in the output from JUnit. More Testing ============ #. 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. #. 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. #. 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! ---- .. [#] 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.