- understand what jUnit is and how it works
- write a simple Java class and some jUnit tests for it
- automate the test running with a simple shell script
- learn some simple optimisation for jUnit tests
- write a test to check for a thrown exception
- execution of the Setup procedure
JUnit is a unit testing framework for the Java programming language. It enables us to write repeatable tests that can be incorporated into a Continuous Integration workflow.
- you may want to fork this repo
- clone the repo to your local machine
- open the project in your editor or IDE
- open a terminal application, and make sure you are in the
step-1-junitdirectory
Note the directory hierarchy in the starter code:
├── src
│ └── Factorial.java
└── test
├── FactorialTest.java
├── hamcrest-core-1.3.jar
└── junit-4.12.jar
There is a clear separation between the source code (the src directory), and tests written against that source (the test directory).
This is a very common pattern used when arranging source code.
Note: the code described below is an adaptation of https://github.com/junit-team/junit4/wiki/Getting-started. To keep things simple, the starter code in this repo already contains the Jar files needed to run the jUnit tests.
We'll take a simple requirement: write a Java class to implement factorial functionality.
Open the empty src/Factorial.java file, enter the following code, and save the file:
public class Factorial {
public int calculate(int start) {
if (start < 0) {
throw new IllegalArgumentException("Factorials are defined only on non-negative integers.");
}
int result = start;
if (result > 1) {
result = start * calculate(start - 1);
}
return result;
}
}NOTE: this is a slightly contrived example, and could be simplified (for example, the
calculate()method only needs to bestatic). However, the main purpose of this example is to enable us to see how a set of jUnit tests can be developed, including the ability to use@Beforeto define steps that are common to all tests.
Consider how you would manually test this. How would you do it?
Hopefully it will be evident that although it would probably be ok to manually test only this class, once other components are introduced into the software, manual testing quickly becomes an inadequate method of testing.
Let's now write a class that jUnit can use to test the Factorial class.
Open the empty test/FactorialTest.java file, enter the following code, and save the file:
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class FactorialTest {
@Test
public void calculate_0() {
Factorial factorial = new Factorial();
int testStartValue = 0;
int expectedResult = 0;
assertEquals(expectedResult, factorial.calculate(testStartValue));
}
}Note each part of that test class:
- imports
- the name of the test class is the class we are going to test, plus
Test - the
@Testannotation indicates the start of a test - the name of the method gives an indicator of what we are testing
- within the test:
- get a new instance of the class we want to test
- and a variable to contain the expected result
- and a variable that contains the actual result
- the naming of these variables describes (documents) their function
In order to be able to run the test, we need to first compile the two Java classes:
javac -classpath .:src:test:test/junit-4.12.jar:test/hamcrest-core-1.3.jar src/Factorial.java test/FactorialTest.javaThen we can run the test:
java -classpath .:src:test:test/junit-4.12.jar:test/hamcrest-core-1.3.jar org.junit.runner.JUnitCore FactorialTestHaving to specify the class path before every command is tiresome. We can automate the whole process by using a simple shell script, which has been provided in the run-tests.bash file in the step-1-junit directory:
./run-tests.bashSample output:
+ export CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ javac Factorial.java FactorialTest.java
+ java org.junit.runner.JUnitCore FactorialTest
JUnit version 4.12
.
Time: 0.015
OK (1 test)
We'll now add a couple more tests, look at a simple way of optimising the tests, and test for a thrown exception when an invalid parameter is provided.
Let's add a test to check the result when we pass in 1 as a parameter:
@Test
public void calculate_1() {
Factorial factorial = new Factorial();
int testStartValue = 1;
int expectedResult = 1;
assertEquals(expectedResult, factorial.calculate(testStartValue));
}Note the simple convention we are using when naming each test – each method is made up of two parts, separated by the _ character:
- the name of the method we are testing (in this case, we are only testing the
calculate()method) - the value passed to the
calculate()method
This is a useful way (but not the only way) to name test methods; you may find other ways. The main thing is for the naming convention to be readable and consistent.
Run the tests again:
./run-tests.bashSample output:
+ export CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ javac Factorial.java FactorialTest.java
+ java org.junit.runner.JUnitCore FactorialTest
JUnit version 4.12
..
Time: 0.012
OK (2 tests)
If you inspect the test class, you will notice that a new instance of the Factorial class is instantiated in each test.
A common pattern you will find in testing frameworks for most programming languages is to be able to specify a block of code that is automatically run before each test.
With jUnit, we use the @Before annotation.
Here's what we will do:
- import the
Beforeannotation - declare a private
factorialvariable to hold theFactorialinstance on the test class – this will be accessible by all tests in the class - add a
@Beforecode block in the test class – this will instantiate theFactorialclass - remove the code that instantiates the
Factorialclass within each test, and refactor it to use the privatefactorialclass variable
Implement each of those changes in
test/FactorialTest.java
Once you have finished making those changes, the test class will look like this:
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Test;
public class FactorialTest {
private Factorial factorial;
@Before
public void setUp() {
factorial = new Factorial();
}
@Test
public void calculate_0() {
int testStartValue = 0;
int expectedResult = 0;
assertEquals(expectedResult, factorial.calculate(testStartValue));
}
@Test
public void calculate_1() {
int testStartValue = 1;
int expectedResult = 1;
assertEquals(expectedResult, factorial.calculate(testStartValue));
}
}Note a key unit testing principle: each test should test one thing, and one thing only. This simplifies each test, and makes it easier to track down any test errors that occur.
We should add another test to check a number greater than 1:
@Test
public void calculate_5() {
int testStartValue = 5;
int expectedResult = 120;
assertEquals(expectedResult, factorial.calculate(testStartValue));
}Then run the tests again.
There is a condition where the parameter passed in is invalid: it is not possible to calculate the factorial value of a negative number. In this case, the factorial() method will throw an IllegalArgumentException.
So we can add a test for this as follows:
@Test(expected=IllegalArgumentException.class)
public void calculate_negative() {
factorial.calculate(-5);
}Note:
- new
@Testsyntax for the annotation - there is no actual assertion within the test itself – this is implicit in the annotation
- and the test will fail if the specified exception is not thrown (test it out yourself!)
Then re-run the tests.
The final output of running ./run-tests.bash should look something like this:
+ export CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ CLASSPATH=.:junit-4.12.jar:hamcrest-core-1.3.jar
+ javac Factorial.java FactorialTest.java
+ java org.junit.runner.JUnitCore FactorialTest
JUnit version 4.12
....
Time: 0.013
OK (4 tests)
- what jUnit is
- how to write jUnit tests
- and automate the testing process with a simple shell script
- and optimise tests using
@Before - and check for an exception thrown when expected