Using JUnit for testing in Java/SCons builds
Current repository revision: 60 (Testing)
Code highlight key
Grey: Code unchanged
Red: Code removed
Green: Code added
Blue: Code not shown→ Denotes the current line and the next are really one line
The code can be checked out with:
svn checkout http://www.mereidea.com/svn/testing/trunk/mi_java_scons
Now that the initial Java / SCons code is in place, some tests can be added. The framework to be used for testing our Java code is JUnit.
This is not really a tutorial on JUnit, but will cover how we installed JUnit on both Linux and Windows, how we tested the installations with a simple test, how we added JUnit based tests and suites into our existing code, how the tests were built with SCons, and how they were run once built.
Installing JUnit
We want to be able to use JUnit on both Linux and Windows environments, and so it was installed on both.
Getting JUnit
At the time of writing, JUnit can be downloaded from github. We just downloaded the Basic jar. The version at writing was junit-4.10.jar.
Linux setup
- Download the JUnit jar file.
- Create a directory to put it in – this example assumes /usr/share/junit/.
- Copy the jar file into the newly created directory.
- Optionally rename the jar from, for example, junit-4.10.jar to junit.jar
- Edit .bashrc or whatever shell initialization file works for you, and add
export CLASSPATH=${CLASSPATH}:/usr/share/junit/junit.jaror whatever line your shell requires to add the directory and file name of the JUnit jar to your class path.
- Save the .bashrc file, re-source it (source .bashrc), or open a new shell, and you’re ready to go!
Windows setup
- Download the JUnit jar file.
- Create a directory to put it in – this example assumes C:\junit\.
- Copy the jar file into the newly created directory.
- Optionally rename the jar from, for example, junit-4.10.jar to junit.jar
- On Vista/Windows7 click the Windows (Start) button, right click on Computer and select Properties. On the window that pops up, select Advanced system settings and, finally, in the dialog that appears click Environment Variables….
- On Windows XP click the Start button, right click on My Computer and select Properties. In the dialog that pops up select the Advanced tab then click the Environment Variables button.
- If the CLASSPATH variable already exists in your System Variables, then select it and click Edit. If it doesn’t exist, click New.
- Either add or enter the path to your junit.jar file (C:\junit\junit.jar)
- Because, on Windows, if the CLASSPATH variable is set, the java command seems to ignore the current directory when searching for classes, ensure . is also in the CLASSPATH variable. For example, our CLASSPATH is:
.;C:\junit\junit.jar
- Click OK, close the dialogs, open a new command prompt, and you’re ready to go!
Using JUnit 4
The version of JUnit we have installed is version 4.10. Before JUnit 4, tests had to extend the TestCase class, and the methods had to have names beginning with test. We use the JUnit 4 annotation methods instead of the inherited TestCase as it seems much simpler and much neater to us. Below the same simple test is written to show both methods of creating tests. The class under test for both is:
public class StringUtils
{
public String concatenateWithSpace(String s1, String s2)
{
return s1+" "+s2;
}
}
Before JUnit 4
As mentioned above, before JUnit 4, the test class for the StringUtils needed to be inherited from TestCase. This can be written:
import junit.framework.*;
public class TestStringUtils extends TestCase
{
private StringUtils str_utils;
protected void setUp()
{
str_utils = new StringUtils();
}
public void testConcatenateWithSpace()
{
String s1 = "Test";
String s2 = "me!";
String expected = "Test me!";
String actual = str_utils.concatenateWithSpace(s1,s2);
assertEquals(actual, expected);
}
}
Compile this with:
javac StringUtils.java TestStringUtils.java
then you can run the test with:
java junit.textui.TestRunner TestStringUtils
With JUnit 4
JUnit 4 enables you to define your tests using annotations. Below is the same test case as before, but rewritten to use JUnit 4. The annotation @Before identifies the setUp method to be called before each test, and the annotation @Test identifies actual tests:
import org.junit.Test;
import org.junit.Before;
import static org.junit.Assert.assertEquals;
public class TestStringUtils
{
private StringUtils str_utils;
@Before
public void setUpStringUtils()
{
str_utils = new StringUtils();
}
@Test
public void concatenateWithSpace()
{
String s1 = "Test";
String s2 = "me!";
String expected = "Test me!";
String actual = str_utils.concatenateWithSpace(s1,s2);
assertEquals(actual, expected);
}
}
Once compiled (as above), this code can be run with:
java org.junit.runner.JUnitCore TestStringUtils
Note: With the JUnit 4 jar you can use either method for writing your tests.
Adding tests to the example code
Now we have written a couple of small, stand-alone tests to check our JUnit installation we can start adding tests to the mi_java_scons code.
Testing the Bar class
The first test we are going to write is for the Bar class in the mi_foo.bar package. In the directory src/mi_foo/bar there is a directory tests and in that directory is a file BarTests.java. This file will contain the tests for the Bar class. Below is part of the code for that class:
... Initial comments omitted ... package mi_foo.bar.tests; // The class to test import mi_foo.bar.Bar; // JUnit stuff import org.junit.Test; import org.junit.Before; import static org.junit.Assert.assertEquals; // Omit comment public class BarTests { // The class under test private Bar bar; // The setup function to create new instances of bar for each test @Before public void setUpBar() { bar = new Bar(); } // Test initialisation @Test public void initialization() { // Check we start at zero assertEquals(0, bar.memoryRecall()); } // Test addition @Test public void addition() { assertEquals(5, bar.add(5)); assertEquals(5+3, bar.add(3)); assertEquals(5+3+7, bar.add(7)); } ... Rest of class omitted ...
This test class is relatively straightforward. We create a new package, mi_foo.bar.tests, to which all the tests in this directory will belong. We then include the class under test and the JUnit classes we want to use. We then define our BarTests class which contains five tests (two shown in the snippet above).
The BarTests class includes a private instance of the Bar class under test. The setUpBar function will ensure that a new instance of the Bar class is created before each test is run.
Note: If we wanted the Bar class to be persistent between tests, then we could create a static set up function that uses the @BeforeClass annotation instead of @Before, and make the Bar member static.
Following the set up function, the actual tests are defined. Each test begins with an @Test annotation. Inside the functions we can use the JUnit asserts to test our Bar class functions are working as expected. These tests tend to use the assertEquals assertion to check the return value of each function call against an expected value. For the expected values we do the sum (e.g. 5+3+7) rather than put the total because it can make it clearer why the result is what we expect, and can also avoid any errors in our mental arithmetic!
Testing the Raw class and its console output
Now that the Bar class is tested, we can move onto the Raw class. This is slightly complicated by the fact that the output of the function call is text written to the console. This is solved by using a ByteArrayOutputStream and using it to create a PrintStream to which the console output is redirected. We can then check the contents of this stream and compare it to the expected output.
In the RawTests.java file, the package is declared as in the BarTests example, and the necessary imports done, including import java.io.* for the streams. Then the test class itself can be created:
... Package declaration and imports omitted ...
public class RawTests
{
//! The class under test
private Raw raw;
//! Handle things sent to System.out
private ByteArrayOutputStream out_str = new ByteArrayOutputStream();
//! The setup function to create a new instance of raw for each test
@Before
public void setUpRaw()
{
raw = new Raw();
System.setOut(new PrintStream(out_str));
}
//! Test solving all world problems
@Test
public void solveAllWorldProblems()
{
raw.solveAllWorldProblems();
String expected = "solveAllWorldProblems: Not Yet Implemented";
expected += System.getProperty("line.separator");
assertEquals(expected, out_str.toString());
}
//! Clean up the data as necessary
@After
public void tearDown()
{
System.setOut(null);
}
}
After including a private instance of the class under test, a private instance of a ByteArrayOutputStream is also contained in the class. The setUpRaw function initializes the Raw class before each test and redirects the console output to the ByteArrayOutputStream.
In the test solveAllworldProblems, the Raw solveAllWorldProblems function is called, and the result (which will be in our stream) tested against an expected string. Note that we have to append System.getProperty(“line.separator”) to the end of our expected string as println is used in the Raw function. As the line separator differs on Windows and Linux, we need to make sure we append the correct system dependent line ending, or the test will fail on one system or the other. If, for example, we use ‘\n’, the test will pass on Linux, but fail on Windows.
Once the test is complete, the @After function is called. This simply redirects stdout back to the console.
This test enables us to test console output, but perhaps a better way is not to use console output directly in the first place. Using one of the logging systems would be a better option for output, and so the next blog post is going to be about adding logging to our code.
Note: Using ByteArrayOutputStream and PrintStream to capture console output was taken from this stackoverflow answer.
Adding a test suite
As we now have more than one test in the bar directory, it would be convenient to be able to run all the tests with one command. This is done by creating a test suite which runs everything in the bar/tests directory. The suite is created in a file in that directory and named MIFooBarTestSuite.java. The name includes the path to the suite. The code for the suite is pretty straightforward:
... Initial comments omitted ...
package mi_foo.bar.tests;
// The JUnit stuff for the suite
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
//! A Suite class to handle the tests
@RunWith(Suite.class)
@Suite.SuiteClasses(
{
BarTests.class,
RawTests.class
}
)
public class MIFooBarTestSuite
{
}
After the package declaration and the imports, the @RunWith annotation is used to denote that this function should be run with the Suite class. We then define which classes we want to run with the Suite class. In this case, BarTests and RawTests. These annotations need to belong to a public class, but no code is needed within that class. How we can run this suite, or individual tests is covered later.
Testing the Foo class
Once the bar sub-directory tests have been set up, the tests in the higher level mi_foo directory can be created.
The test for the Foo class is very similar to the test of Raw, except the building of the expected string is a little more complicated:
... Initial comments and setUp omitted ... //! Test displaying a string @Test public void displayFoo() { String disp_string = "Test Display String"; foo.displayFoo(disp_string); String lsep = System.getProperty("line.separator"); String expected = "solveAllWorldProblems: Not Yet Implemented"; expected += lsep; expected += "foobar.Foo showing string: \n\t"; expected += disp_string + lsep; assertEquals(expected, out_str.toString()); } ... tearDown function omitted ...
In the Foo class displayFoo function, “\n\t” is used for display formatting as well as using println. println and ‘\n’ don’t produce the same result (on Windows, at least), so where ‘\n’ is used in the display, ‘\n’ should also be put into the expected string, and where println is used we use the line separator.
Creating a hierarchical suite
It is useful to create a suite that can run the tests in the mi_foo directory as well as the tests for code it uses (in this case the tests in mi_foo/bar). To do this we create a hierarchical test suite that includes the Foo tests and the suites of any directories below the mi_foo directory. This is done in a file in mi_foo/tests that is named MIFooTestSuite.java:
... Initial comments omitted ...
package mi_foo.tests;
// Import the sub tests
import mi_foo.bar.tests.MIFooBarTestSuite;
// The JUnit stuff for the suite
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
//! A Suite class to handle the tests
@RunWith(Suite.class)
@Suite.SuiteClasses(
{
MIFooBarTestSuite.class,
FooTests.class
}
)
public class MIFooTestSuite
{
}
This is similar to the earlier suite, except that we import the MIFooBarTestSuite and add that to the SuiteClasses. This suite will now run everything in the mi_foo/bar/tests suite as well as the tests in this directory.
Adding a ‘run all’ suite
It is useful to be able to make a single call that will run all the tests. As it happens, we only have one module at the moment, so running MIFooTestSuite will do that, but as we add other modules we need a top-level class to run all suites for all modules. This suite is created in the top src directory in a file named TestRunner.java:
... Initial comments omitted ...
// Import the main test suites from the sub packages
import mi_foo.tests.MIFooTestSuite;
// The JUnit stuff for the suites
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
//! A Suite class to handle all the tests
@RunWith(Suite.class)
@Suite.SuiteClasses(
{
MIFooTestSuite.class
}
)
public class TestRunner
{
}
Given the earlier suites, this code is simple enough. The MIFooTestSuite is imported and run by this suite. When other modules are created, their top-level suites will be included in this file and run. This is all we have to do as running MIFooTestSuite will run the tests in mi_foo and all the test suites below it.
Building the tests
Now we have all our test code in place, we want to be able to build it with SCons. Only one small change is required to the SConstruct file to do this, and that change is to ensure the CLASSPATH variable is set from the system environment:
... # On Windows, the path is incorrect, so change the path to # the environment one. Even though windows is the problem, # do it for all platforms newpath=os.environ.get('PATH') env.Append(ENV = { 'PATH' : newpath }) # Set the classpath to the CLASSPATH environment variable # for finding JUnit and maybe others cpath=os.environ.get('CLASSPATH') env.Append(JAVACLASSPATH = cpath) # On Windows the Jar builder isn't loaded properly, so do # it explicitly SCons.Tool.jar.generate(env) # Build the lot with a single command! env.Java(env['bdir'], env['sdir']) ...
Now in the the mi_java_scons directory containing the SConstruct file we can type scons and the result should be something like:
> scons scons: Reading SConscript files ... scons: done reading SConscript files. scons: Building targets ... javac -classpath :/usr/share/junit/junit.jar -d build → -sourcepath src src/TestRunner.java src/mi_foo/Foo.java → src/mi_foo/tests/FooTests.java → src/mi_foo/tests/MIFooTestSuite.java → src/mi_foo/bar/Bar.java src/mi_foo/bar/Raw.java → src/mi_foo/bar/tests/BarTests.java → src/mi_foo/bar/tests/MIFooBarTestSuite.java → src/mi_foo/bar/tests/RawTests.java scons: done building targets.
Running the tests
Once built the tests can be run by changing into the build directory and calling the appropriate class with the org.junit.runner.JUnitCore test runner. For example, to run all the tests, we simply use the TestRunner class:
> java org.junit.runner.JUnitCore TestRunner JUnit version 4.10 ....... Time: 0.023 OK (7 tests)
If we want to just run the tests in the mi_foo package (actually the same as all tests at the moment), still in the build directory, type:
> java org.junit.runner.JUnitCore mi_foo.tests.MIFooTestSuite JUnit version 4.10 ....... Time: 0.021 OK (7 tests)
To only run the tests in the mi_foo.bar package, again in the build directory, type:
> java org.junit.runner.JUnitCore mi_foo.bar.tests.MIFooBarTestSuite JUnit version 4.10 ....... Time: 0.019 OK (6 tests)
Finally, to run the tests for a single class (the tests for the Bar class for example), inside the build directory, simply type:
> java org.junit.runner.JUnitCore mi_foo.bar.tests.BarTests JUnit version 4.10 ..... Time: 0.014 OK (5 tests)
Any test or suite of tests can be easily run in a similar way.
Summary
This post covered how we added tests to our existing mi_java_scons code using JUnit. We covered the installation of JUnit on both Windows and Linux, and how those installations were tested using a simple test example. The test example used both the pre-JUnit 4 method of extension of classes, and the JUnit 4 method of using annotations.
The annotation method was then used to add tests to the mi_java_scons code, creating tests for each class in the current code. Test suites were also created to collect together the tests at each level and enable them to be run with one call. A hierarchy of suites enabled one test suite to call another until, at the top-level, a single suite could be called to run all tests.
How this code was built using SCons was then described, followed by how the tests could be run once built.
- Revision 51
- Made some methods public due to a mistake in the initial draft of the classes
- Revision 52
- Added the class path to the SConstruct and renamed all the test classes from *Test.java to *Tests.java. Added tests for Bar and Raw classes
- Revision 53
- Changed line ending for expected string in RawTests to use the System.getProperty(“line.separator”) to work properly on Windows
- Revision 54
- Updated some Doxygen comments
- Revision 55
- Added test suites and the TestRunner so that multiple tests can be run with one call
- Revision 56
- Changed expected string in FooTests as ‘/n’ and System.getProperty(“line.separator”) do not produce the same output
- Revision 57
- Changed the Doxygen version filter to use awk instead of sed
- Revision 59
- Changed the README to correctly describe how to run the tests
- Revisions 58 and 60
- Changes to other, unrelated code in the Testing repository
Frequent updates on Twitter





