Testing Methods with Unit Tests with JUnit 4

One of the key components of writing good software is to ensure that our code is tested. Clearly we can test our applications by running them and checking that we receive the expected response, but in anything but the simplest of apps, some errors can be hard to track - particularly if they do not cause the application to crash. Consider the following code

package uk.ac.chester;

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner inputScanner = new Scanner(System.in);
        System.out.println("Please enter a phrase:");
        String line = inputScanner.nextLine();
        char letter = line.charAt(0);
        char upperLetter = convertToUpperCase(letter);
        System.out.println(upperLetter);
    }

    public static char convertToUpperCase(char letter) {
        String letterAsString = String.valueOf(letter);
        letterAsString = letterAsString.toUpperCase();
        char upperLetter = letterAsString.charAt(0);
        return upperLetter;
    }

}
In order to test the convertToUpperCase() method we are required to write correct code to use the Scanner, get a string, get a character in a string and then print the output. Even if we wrote code to create the char and modified it to test multiple letters it is still an unwieldy way to test things. Consider if it was part of a large programme, where we couldn't just modify the programmes core functionality to test the one method, it would be very hard to check that the method works correctly.

A solution to this problem is to write code which has the sole purpose of testing a method. The next steps walk you through the process of creating a unit test for the convertToUpperCase method

  1. Create a command line project in IntelliJ IDEA with the code shown above
  2. Place the cursor in the name of the method we want to test, selecting 'Navigate' on the menu and choosing 'test' or press ⇧⌘T (Ctrl + Shift + T on Windows)
  3. Choose the create test option
  4. In the screen that appears:
    • Set the testing library to JUnit4
    • Click the 'Fix' button and in the dialogue box that appears choose the 'Use 'JUnit4' from IntelliJ IDEA distribution' option
    • Check the checkbox for creating a unit test for the convertToUpperCase() method
    As shown here Setting test creation options in IntelliJ IDEAClick OK and a file will be created called MainTest, which will look similar to that shown below:
    package uk.ac.chester;
    import static org.junit.Assert.*;
    /**
     * Created by Andrew on dd/mm/yyyy.
     */
    public class MainTest {
        @org.junit.Test
        public void convertToUpperCase() throws Exception {
        }
    }

    The word Assert on the top line will be red, to resolve this error, click on it, press Alt+Enter and choose the 'Add library 'JUnit4' to classpath option, as shown here: Adding the JUnit library to the classpath

    The method that is created is designated as a testing method by the @Test code, the additional syntax that you may not have seen before: throws Exception means that the code can be expected to error (which, if we write our tests correctly) it would if there is an error in the convertToUpperCase() method in the Main class.

    You may also notice that the method is inside a new class called MainTest this convention indicates that methods here will test the corresponding methods in the Main class

  5. The next stage is to write some code to test the method, we do this using a special class called 'Assert', which allows us to check various things, such as if values are equal, or if a value is true. Modify the methods as shown below:
    package uk.ac.chester;
    import org.junit.Assert;
    
    /**
     * Created by Andrew on dd/mm/yyyy.
     */
    public class MainTest {
        @org.junit.Test
        public void convertToUpperCase() throws Exception {
            char resultWithA = Main.convertToUpperCase('a');
            Assert.assertEquals("a must convert to A", 'A', resultWithA);
            }
    }
    The three changes made are as follows:
    1. An import statement is added to include the 'Assert' package which we will use in the body of the test method
    2. A line of code calls the convertToUpperCase() method in the Main class with the lowercase letter 'a' as the parameter and stores the result as a char
    3. A line of code asserts that the expected value (a capital 'A') is equal to that actual value returned by the method. We pass a string into the method which is printed if the test fails, so we know where the error is to be found
    It's worth being aware that the order for parameters in the assertEquals methods is <Condition>,<Expected result>,<Actual result>. If the last two are incorrectly ordered, then error messages may not make sense.
  6. To run the test, click the play button in the gray bar to the left of the method code IntelliJ Unit test play button:
  7. To the bottom of the screen you should see output similar to that shown below, indicating that the test has passed successfully as follows IntelliJ IDEA successful test completion pane Verify this is as expected, if not check that your code is written accurately.
  8. In IntelliJ, go back to the Main class, comment out the existing code in the covertToUpperCase method and add a new statement to return just an uppercase 'A' as follows:
        public static char convertToUpperCase(char letter) {
    //        String letterAsString = String.valueOf(letter);
    //        letterAsString = letterAsString.toUpperCase();
    //        char upperLetter = letterAsString.charAt(0);
    //        return upperLetter;
            return 'A';
        }
  9. Return to the test and run it. You should see it pass, despite the fact we have modified the code so it is clearly incorrect.
  10. As the test is clearly unsatisfactory, add another method to check a different letter as follows
    char resultWithF = Main.convertToUpperCase('f');
    Assert.assertEquals("f must convert to F", 'F', resultWithF);
  11. Run the test again, and the new assertion should fail, with error output similar to that shown below: IntelliJ IDEA failed test outputNote that the error string from the test method it output, enabling us to identify which assertion is failing. We can also see actual and expected results in the output. Because chars have an underlying numeric representation, we see expected values as integers here, with 70 being the Unicode number for 'F' and 65 being the Unicode number for 'A'
  12. Go back to the main class and undo the changes you made earlier.
  13. Re-run the test and confirm it passes
  14. At this stage, we have only written code to check conversion of lowercase letters to upper, and for greater assurance that our code will behave as intended, we need to consider a wider range of possible inputs and expected outputs, for example if an uppercase 'C' is passed into the method we would expect that it remains as an uppercase 'C', if a number '4' character is used we would expect that to be unchanged, and if a lowercase 'ü' is passed in, it should retain its umlaut following conversion. Add methods as follows to do that:
    char resultWithUpperC = Main.convertToUpperCase('C');
    Assert.assertEquals("C must be unchanged", 'C', resultWithUpperC);
    
    char resultWith4 = Main.convertToUpperCase('4');
    Assert.assertEquals("4 must be unchanged", '4', resultWith4);
    
    char resultWithUmlautU = Main.convertToUpperCase('ü');
    Assert.assertEquals("ü must convert to Ü", 'Ü', resultWithUmlautU);
  15. Verify the tests pass correctly

The process of writing tests requires a judgement call to be made my the developer. What are the likely errors to guard against in any given method, and how much time is required to write the tests. At this stage, writing tests may take a long time, for little perceivable gain, however as you become a more proficient programmer, you will become faster at writing tests, and more able to discern which tests should be written.

A further benefit of writing unit tests is that if you make a change to a method (for example you refactor it), it is very quick to re-run the tests to check you haven't broken it!

Switching between running tests and running the programme

At the top right hand corner of IntelliJ IDEA is a dropdown list - this shows both the test application and the main application - use the drop down to switch between them so the 'play' button will run the desired application. Ignore the edit and save options for now. IntelliJ IDEA configuration switcher

Task

  1. Add the following code to the Main class of the above project:
    public static String makeTitleCase(String phrase) {
        String result = "";
        boolean startOfWord = true;
        for (int i = 0; i < phrase.length(); i++) {
            char letter = phrase.charAt(i);
            if (letter == ' ') {
                startOfWord = true;
            } else {
                if (startOfWord) {
                    letter = convertToUpperCase(letter);
                    startOfWord = false;
                }
            }
            result += letter;
        }
        return result;
    }
  2. Modify the code in the main method to request text from the user, then call the makeTitleCase method (with that text) and finally print the result to the screen. Verify that typing 'hello world' (without the quotes) results in 'Hello World' being printed.
  3. Add a unit test for this method
  4. Add assertions in the unit test until you find the bug in the code (for the avoidance of doubt, the behaviour of this method should be the same as Microsoft Word's Change case -> Title Case feature. This feature is also labelled as 'Capitalize Each Word' in some versions of Word)
  5. Fix the bug in the code (If you write a new method to do this, it will also need its own unit test).
  6. Check all the unit tests pass
A video walkthrough of this task is available

Advanced task

Revisit previous tasks you have completed. Write unit tests to verify the methods in them. Where all the code is in the main method, extract methods as appropriate before writing the unit tests.