| JUnit Primer |
| Summary | |
|
|
|
Mike Clark October 7, 2000 |
| Introduction |
The goal of this article is to demonstrate a quick and easy way to write and run JUnit test cases and test suites. We'll start by reviewing the benefits of using JUnit and then write some example Java code to demonstrate its effectiveness.
This article contains the following sections:
Before you start, make sure you have downloaded and installed the following software:
| Why Use JUnit? |
It's worth asking why we should use JUnit at all. The subject of unit testing always conjures up visions of long nights slaving over a hot keyboard trying to meet the project's test case quota. However, unlike the draconian style of conventional unit testing, using JUnit actually helps you write code faster while increasing code quality by creating synergy between coding and testing.
Here are just a few reasons for using JUnit:
Yeah, I know, it sounds counter-intuitive -- but it's true! When you write tests using JUnit, you'll spend less time debugging, and you'll have confidence that changes to your code actually work. Without tests, it's easy to become paranoid about refactoring or changing code because you don't know what might break as a result. By writing tests, you can quickly re-run the tests after changing the code and gain confidence that your changes didn't break anything. If a bug is detected while running tests, the source code is fresh in your mind, so the bug is easily found. Tests written in JUnit help you write code at an extreme pace and spot defects quickly.
Writing tests should be simple - that's the point! If writing tests is too complex or takes too much time, then there's no incentive to start writing tests in the first place. With JUnit, you can quickly write tests that exercise the interfaces of your components. Once you've written some tests, you want to run them quickly and frequently without disrupting the creative design and development process. With JUnit, running tests is as easy and fast as running a compiler on your code. In fact, you should run your tests every time you run the compiler.
Testing is no fun if you have to manually compare the expected and actual outcome of tests, and it slows you down. JUnit tests can be run automatically and they check their own results. When you run tests, you get simple and immediate feedback as to whether the tests passed or failed. There's no need to comb through a report of test results.
JUnit tests can be logically grouped into test suites which can then be run automatically. This allows for automatic regression testing of a collection of tests to an arbitrary depth.
Using the JUnit testing framework, you can write tests and enjoy the convenience of the testing harness supported by the framework. Writing a test is as simple as writing a method which exercises the code to be tested and defining the expected result. The framework provides the environment in which the test can be run automatically and as part of a collection of other tests.
The fewer tests you write, the less stable your code becomes. Tests validate the stability of the software and instill confidence that changes haven't caused a ripple-effect through the software. The tests form the glue of the structural integrity of the software.
JUnit tests are highly localized tests written to improve a developer's productivity and code quality. Unlike functional tests, which treat the system as a black box and ensure that the software works as a whole, unit tests are written to test the fundamental building blocks of the system from the inside out. Developer's write and own the JUnit tests. When a development iteration is complete, the tests are promoted as part and parcel of the delivered component as a way of communicating, "Here's my deliverable and the tests which verify it."
Testing Java software using Java tests forms a seamless bond between the test and the code under test. The tests become an extension to the overall software. The Java compiler helps the testing process by performing static syntax checking of the unit tests and ensuring that the software interface contracts are being obeyed.
| Design Of JUnit |
JUnit is designed around two key design patterns: the Command pattern and the Composite pattern.
A TestCase is a command object. Any class that contains test methods
must subclass the TestCase class. A TestCase can define
any number of testXXX() methods. When you want to check the expected
and actual test results, you invoke any variation of the assert()
convenience method and pass a boolean expression that returns
true to indicate the assertion is true. TestCase subclasses
which contain multiple testXXX() methods can use the setUp()
and tearDown() convenience methods to initialize and release any
common objects under test.
TestCase instances can be composed into
TestSuite hierarchies, which automatically invoke all the
testXXX() methods defined in each TestCase instance.
A TestSuite is a composite of other tests, either TestCase
instances or other TestSuite instances. TestSuite instances
and TestCase instances can be added to a TestSuite
using the addTest() convenience method. This supports suites
of suites of suites of tests, to an arbitrary depth. It's the power of this composite
behavior that makes it easy to compose a hierarchy of test suites and test cases and
run all the tests automatically and uniformly.
| Step 1: Write A Test Case |
First, we'll write a test case to exercise a single software component. We'll focus on writing tests that exercise the component behavior that has the highest potential for breakage, thereby maximizing our return on testing investment.
To write a test case, follow these steps:
TestCase.
setUp() method to initialize object(s) under test.
tearDown() method to release object(s) under test.
testXXX() methods which exercise the object(s) under test.
suite() factory method which creates a TestSuite
containing all the testXXX() methods of the TestCase.
main() method for running the TestCase.
The following is an example test case:
(The complete source code for this example is available in the Resources section).
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class ShoppingCartTest extends TestCase {
private ShoppingCart _bookCart;
/**
* Constructs a ShoppingCartTest with the specified name.
*
* @param name Test case name.
*/
public ShoppingCartTest(String name) {
super(name);
}
/**
* Sets up the text fixture.
*
* Called before every test case method.
*/
protected void setUp() {
_bookCart = new ShoppingCart();
Product book = new Product("Extreme Programming", 23.95);
_bookCart.addItem(book);
}
/**
* Tears down the text fixture.
*
* Called after every test case method.
*/
protected void tearDown() {
_bookCart = null;
}
/**
* Tests the emptying of the cart.
*/
public void testEmpty() {
_bookCart.empty();
assert(_bookCart.isEmpty());
}
/**
* Tests adding a product to the cart.
*/
public void testProductAdd() {
Product book = new Product("Refactoring", 53.95);
_bookCart.addItem(book);
double expectedBalance = 23.95 + book.getPrice();
double currentBalance = _bookCart.getBalance();
double tolerance = 0.0;
assertEquals(expectedBalance, currentBalance, tolerance);
int expectedItemCount = 2;
int currentItemCount = _bookCart.getItemCount();
assertEquals(expectedItemCount, currentItemCount);
}
/**
* Tests removing a product from the cart.
*
* @throws ProductNotFoundException If the product was not in the cart.
*/
public void testProductRemove() throws ProductNotFoundException {
Product book = new Product("Extreme Programming", 23.95);
_bookCart.removeItem(book);
double expectedBalance = 23.95 - book.getPrice();
double currentBalance = _bookCart.getBalance();
double tolerance = 0.0;
assertEquals(expectedBalance, currentBalance, tolerance);
int expectedItemCount = 0;
int currentItemCount = _bookCart.getItemCount();
assertEquals(expectedItemCount, currentItemCount);
}
/**
* Tests removing an unknown product from the cart.
*/
public void testProductNotFound() {
try {
Product book = new Product("Ender's Game", 4.95);
_bookCart.removeItem(book);
fail("Should raise a ProductNotFoundException");
} catch(ProductNotFoundException pnfe) {
// should never get here
}
}
/**
* Assembles and returns a test suite for
* all the test methods of this test case.
*
* @return A non-null test suite.
*/
public static Test suite() {
//
// Reflection is used here to add all
// the testXXX() methods to the suite.
//
TestSuite suite = new TestSuite(ShoppingCartTest.class);
//
// Alternatively, but prone to error when adding more
// test case methods...
//
// TestSuite suite = new TestSuite();
// suite.addTest(new ShoppingCartTest("testEmpty");
// suite.addTest(new ShoppingCartTest("testProductAdd");
// suite.addTest(new ShoppingCartTest("testProductRemove");
// suite.addTest(new ShoppingCartTest("testProductNotFound");
//
return suite;
}
/**
* Run the test case.
*/
public static void main(String args[]) {
String[] testCaseName = {ShoppingCartTest.class.getName()};
//junit.textui.TestRunner.main(testCaseName);
//junit.swingui.TestRunner.main(testCaseName);
junit.ui.TestRunner.main(testCaseName);
}
}
| Step 2: Write A Test Suite |
Next, we'll write a test suite that includes several test cases. The test suite will allow us to run all of its test cases in one fell swoop.
To write a test suite, follow these steps:
TestCase.
suite() factory method which creates a TestSuite
containing all the TestCase instances and TestSuite
instances contained in the TestSuite.
main() method for running the TestSuite.
The following is an example test suite:
public class EcommerceTestSuite extends TestCase {
/**
* Constructs a EcommerceTestSuite with the specified name.
*
* @param name Test suite name.
*/
public EcommerceTestSuite(String name) {
super(name);
}
/**
* Assembles and returns a test suite
* containing all known tests.
*
* New tests should be added here.
*
* @return A non-null test suite.
*/
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(ShoppingCartTest.suite());
suite.addTest(CreditCartTestSuite().suite());
return suite;
}
/**
* Runs the test suite.
*/
public static void main(String args[]) {
String[] testCaseName = {EcommerceTestSuite.class.getName()};
//junit.textui.TestRunner.main(testCaseName);
//junit.swingui.TestRunner.main(testCaseName);
junit.ui.TestRunner.main(testCaseName);
}
}
| Step 3: Run The Tests |
Now that we've written a test suite containing a collection of test cases and
other test suites, we can run either the test suite or any of its test cases
individually. Running a TestSuite will automatically run all of
its subordinate TestCase instances and TestSuite instances.
Running a TestCase will automatically invoke all of its defined
testXXX() methods.
JUnit provides both a graphical and a textual user interface. Both
user interfaces indicate how many tests were run, any errors or failures,
and a simple completion status. The graphical user interface displays either an
AWT-based (junit.ui.TestRunner) or Swing-based (junit.swingui.TestRunner)
window that displays a green progress bar if all the tests passed or a red progress
bar if any of the tests failed. The textual user interface (junit.textui.TestRunner)
displays "OK" if all the tests passed and failure messages if any of the tests failed.
In general, TestSuite and TestCase classes should
define a main() method which employs the appropriate user interface.
The tests we've written so far have defined a main() using the
AWT-based user interface.
Running our test case is simple:
java ShoppingCartTest
Running our test suite, and all of its test cases, is equally simple:
java EcommerceTestSuite
The simplicity of the user interfaces is the key to running tests quickly. You should be able to run your tests and know the status with a glance, much like you do with a compiler.
| Step 4: Organize The Tests |
The last step is to decide where the tests will live within our development environment.
Here's the recommended way to organize tests:
com.mydotcom.ecommerce package would have a
test package defined in com.mydotcom.ecommerce.test
package.
com.mydotcom.ecommerce.test package, define a
TestSuite class that contains all the tests for verifying
the code in this package.
TestSuite classes that create higher-level and
lower-level test suites in the other packages (and sub-packages) of
the application.
By creating a TestSuite in each Java package, at various levels of packaging,
you can run a TestSuite at any level of abstraction. For example, you
can define a com.mydotcom.test.MasterTestSuite which runs all the tests
in the system and a com.mydotcom.ecommerce.test.EcommerceTestSuite which
runs only those tests for the e-commerce classes.
The testing hierarchy can extend to an arbitrary depth. Depending on the level of abstraction you're developing at in the system, you can run an appropriate test. Just pick a level of abstraction in the system and test it!
Here's an example testing hierarchy:
MasterTestSuite - The top-level test suite
SmokeTestSuite - Structural integrity tests
EcommerceTestSuite
ShoppingCartTestCase
CreditCartTestSuite
AuthorizationTestCase
CaptureTestCase
VoidTestCase
UtilityTestSuite
MoneyTestCase
DatabaseTestSuite
ConnectionTestCase
TransactionTestCase
LoadTestSuite - Scalability tests
DatabaseTestSuite
ConnectionPoolTestCase
ThreadPoolTestCase
| Testing Idioms |
Keep these things in mind when testing:
System.out.println(),
write a test case instead.
| Load Testing Extensions |
The JUnit Load Testing Extensions were developed as extensions to the JUnit framework for the purpose of assemblying and executing performance and scalability tests.
BaseTestCase
A base class for test cases which includes some convenience methods for executing test cases.
Any BaseTestCase, or TestSuite which extends the BaseTestCase, can be run as follows:
java <TestName> // Graphical UI java <TestName> -text // Text UI
TimedTest
A TestCase decorator that runs the decorated TestCase and displays the begin, end, and elapsed time in milliseconds. Each instance of a TimedTest has a specified maximum elapsed time, and if the max time is exceeded, then the test case fails.
The following is an example of how to construct a TimedTest:
//
// Creates a timed test of the testSomething() method
// with a max elapsed time of 2 seconds.
//
Test t = new MyTestCase("testSomething");
Test timedTest = new TimedTest(t, t.getName(), 2000);
Timer
An interface which defines the methods that must be implemented by pluggable
timers. The ConstantTimer has a constant delay, and the
RandomTimer has a random delay with a uniformly distributed variation.
LoadTestCase
A TestCase which has a specified number of maximum users and an elapsed
time. This class provides a makeLoadTest() factory method which
decorates the LoadTestCase as a timed and threaded test. Once a LoadTestCase has
been decorated in this fashion, all the methods registered in the suite
will be run as an atomic unit concurrently with the specified maximum number
of simulated users. If the elapsed time is exceeded, then the test case
fails.
ExampleLoadTest
A LoadTestCase which tests the creation of random numbers.
UrlLoadTest
A LoadTestCase which tests the response time of a collection of URLs.
LoadTestSuite
The top-most test suite used to run all known load tests. It includes two example load test cases - ExampleLoadTest and UrlLoadTest. You won't want to run this as part of your defect testing suite, as it will defeat the purpose of rapid feedback. This is a candidate for the lunch hour test suite.
The following is an example LoadTestCase:
public class ExampleLoadTest extends LoadTestCase {
/**
* Constructs an ExampleLoadTest with the specified name.
*
* The test simulates 10 concurrent users ramping
* at 1 user per second with a maximum elapsed time
* of 2 minutes.
*
* @param name Test name.
*/
public ExampleLoadTest(String name) {
super(name);
setMaxUsers(10);
setTimer(new ConstantTimer(1000));
setMaxElapsedTime(120000);
}
/**
* Sets up the test fixture.
*/
protected void setUp() {
super.setUp();
}
/**
* Tears down the test fixture.
*/
protected void tearDown() {
super.tearDown();
}
/**
* Assembles and returns a test suite for all
* the test methods of this class.
*
* All the load-related methods should be added here.
*
* @return A non-null test suite.
*/
public static Test suite() {
TestSuite suite = new TestSuite();
suite.addTest(makeLoadTest(new ExampleLoadTest("testRandomGen")));
return suite;
}
/**
* Example load test.
*/
public void testRandomGen() {
java.util.Random r = new java.util.Random();
for (int i=0; i < 100000; i++) {
r.nextDouble();
}
}
/**
* Test main.
*/
public static void main(String args[]) {
String[] testCaseName = {ExampleLoadTest.class.getName()};
//junit.textui.TestRunner.main(testCaseName);
//junit.swingui.TestRunner.main(testCaseName);
junit.ui.TestRunner.main(testCaseName);
}
}
| Resources |
| About the author | |
|