Section 18.3. JUnit Overview


18.3. JUnit Overview

With this introduction to the concepts of unit testing behind us, let's dive into the technical details of using JUnit to create and execute unit tests on basic Java code. In a later section, we'll see how Cactus can be used to test code that must run in a J2EE container.

JUnit is a framework for writing unit tests that consists of a Java API that you use to write your tests and a set of basic tools that can be used to run your test suites and report on the results.

The JUnit API is provided as two packages: junit.framework contains the core classes that you'll typically use to define and execute your unit tests while junit.extensions contains some potentially useful extensions to the core framework classes.

In the JUnit model, tests themselves are represented as TestCase objects. A set of related tests is organized into a TestSuite. A TestRunner actually drives a suite of tests and reports on the results. All of these classes are in the junit.framework package. The JUnit framework has several other classes and interfaces, but these are the primary ones that you'll need to understand to get started. In keeping with the principle of simplicity we discussed earlier, the JUnit authors kept the class model in the framework as simple as possible.

The JUnit runtime model is also fairly simple. In a nutshell, executing tests using JUnit typically involves the following steps:

  1. You invoke a TestRunner on a TestSuite. Some TestRunner implementations allow you to specify an individual TestCase to be run, but in those cases, the TestRunner simply wraps the TestCase with a TestSuite and runs the suite. The TestRunner examines the suite of tests by iterating through the suite, finding each TestCase contained in the suite.

  2. For each TestCase in the suite, the TestRunner determines which tests are to be run. When adding a TestCase to the suite, you can specify that all tests on the TestCase are to be run, in which case any method whose name starts with "test" is considered a test method. Alternatively, you can specify individual test methods on the TestCase, as described in a later section.

  3. For each test method to be run, the TestRunner calls the TestCase's setUp( ) method, then invokes its runTest( ) method to execute the test method itself. If the specified test method is not found or is inaccessible (nonpublic), a test failure is generated. You can override the runTest( ) method in your TestCase implementations, but this isn't normally necessary unless you're defining a specialized test harness of some kind. Once the test method completes (either successfully or with an error or exception of some kind), the tearDown( ) method on the TestCase is invoked, allowing you to clean up any resources set up for the test. The tearDown( ) method is called whether the test succeeds, fails, or generates an uncaught exception.

  4. If a test method completes without an exception or error being thrown, the test is marked as passed. If the test method throws an AssertionFailedError (e.g., an assert method call in the test method failed, or one of the fail( ) methods was called in the test method), the test is marked as failed. If an uncaught exception of any other type is thrown by the test method (e.g., a call in the test method generates a NullPointerException that is not caught and handled in the test method), the test is marked as an error, meaning that the test encountered an error that prevented a positive or negative result from being recorded.

  5. Most TestRunner implementations report on the status of each test as it is run. The text-based junit.textui.TestRunner, for example, prints an F character on the console for each failed test, an E for each test that resulted in an error, and an unobtrusive dot (.) for each passed test. Once all of the test methods (in aggregate) have been run, the TestRunner reports on the total results, typically displaying statistics on the number of successful versus failed tests.

So from your perspective as a developer, unit testing with JUnit involves writing one or more TestCase subclasses with appropriate test methods on each, optionally organizing them into TestSuites, and then executing suites of tests through a TestRunner.

18.3.1. Defining Tests by Writing TestCases

A TestCase is meant to represent a set of tests targeted at a specific unit of code. The scope of the "unit of code" is up to you and how you decide to organize your tests overall, but typically a TestCase is aimed at a specific class in your overall model. You implement tests for your code unit by subclassing TestCase and defining test methods on the subclass. Each test method name must start with "test" and must have the signature:

 public void testXXX(  ) { . . . } 

This method-naming convention allows the JUnit framework to find test methods through introspection when automatically invoking tests, as described in the previous section.

Traditionally, each TestCase corresponds to a class to be tested, but nothing in the JUnit framework enforces this practice. You can write multiple TestCases to test a single class in your design, or a single TestCase can include tests for multiple classes in your system. The traditional approach of creating one TestCase for each class is motivated by the need to assess test coverage and to isolate tests from each other (or at least understand their dependencies). Classes provide a natural organizing principle in both of these areas.

To demonstrate, Example 18-1 shows a TestCase written for a Person Java bean. The Person class under test, as well as the rest of the application code used to generate examples in this chapter, are taken from the PeopleFinder sample application used in Chapters 2 and 17.

Example 18-1. Simple TestCase for the Person class
 import junit.framework.TestCase;   public class TestPerson extends TestCase {     /** Default constructor */     public TestPerson (  ) {         super(  );     }       /** Constructor with a name for the test case method */     public TestPerson (String name) {         super(name);     }       /** Test the name accessors on the Person bean */     public void testSetName(  ) {         // Create a fresh person         Person p = new Person(  );         // Set the first and last name fields         String fName = "John";         String lName = "Smith";         p.setFirstName(fName);         p.setLastName(lName);         // Verify that the values were accepted and saved         assertNotNull(p.getFirstName(  ));         assertEquals(p.getFirstName(  ), fName);         assertNotNull(p.getLastName(  ));         assertEquals(p.getLastName(  ), lName);     }       /** Test the addition of email addresses */     public void testAddEmail(  ) {         // Create a fresh person         Person p = new Person(  );         // Set the email address         String addr = "jsmith@anywhere.com";         p.addEmailAddress(addr);         // There should be only a single email address on the person         assertTrue((p.getEmailAddresses(  ).size(  ) == 1));         // The email address should be the value we just added         String addrR = (String)p.getEmailAddresses().iterator(  ).next(  );         assertNotNull(addrR);         assertEquals(addr, addrR);     } } 

The test class is called TestPerson, which follows a common convention used in JUnit with the TestCase class name derived by prepending "Test" to the name of the unit under test. Aside from two constructors that we include here to match those provided by the TestCase base class, the entire class consists of two test methods. The first, testSetName( ), exercises and verifies the operation of the name accessors on the Person bean, and the other, testAddEmail( ), tests the addition of email addresses to a person.[2] In each of our tests, we first set up some test data by constructing a Person instance and setting some of its properties, then test for the expected results using the assertXXX( ) methods available in any TestCase.

[2] It's important to note here that some of our unit test examples are intentionally oversimplified. In the case of testAddEmail( ), for example, it's unlikely that you will unit test every accessor in a real-world application. The number of unit tests would explode, and their usefulness would be suspect anyway since the implementation of setters and getters are typically extremely simple and not worthy of their own unit tests. But they do make for good examples to demonstrate basic JUnit usage.

18.3.2. Making Assertions in Test Methods

The assertion methods used in a TestCase are inherited from the Assert base class that TestCase extends. You invoke these assertion methods in the test methods on your TestCases. If the assertion passes, the method completes normally and the test method continues. If the assertion fails, an exception is thrown, the test method halts, and a failure is reported by the TestRunner.

Several flavors of assertions are provided for you to use in your test methods:


assertEquals( )

These assertion methods allow you to assert that two items have equal values. Several versions are available, allowing you to compare objects and data items of various types (for example, Strings, ints, and Objects).


assertFalse( )/assertTrue( )

These methods can be used to test a predicate (represented as a boolean argument) for either truth or falsity.


assertNull( )/assertNotNull( )

These assertions involve checking that a given object is either null or not null.


assertSame( )/assertNotSame( )

These assertions check to see whether two objects refer to the same object or not.

In our testSetName( ) method, for example, we used the assertNotNull( ) method to assert that the first and last name attributes should not be null after they have been set. We also used the assertEquals( ) method to assert that the values should be equal to the intended values.

18.3.3. Failing Tests

A failed assertion causes a test to fail, but in some cases you may need to explicitly fail a test when you encounter a particular condition in your test method. For these situations, all TestCases also inherit the fail( ) methods from the Assert base class. Calling fail( ) causes the current test method to fail immediately. You might need to fail a test explicitly if, for example, expected or unexpected exceptions are thrown in your test code. Adding the following test to our TestPerson test case, for example, tests the correct behavior of the equals( ) method on the Person bean:

 public void testEquals(  ) {     // Set up a test person with some data     mPerson.setFirstName("John");     mPerson.setLastName("Jones");     mPerson.addEmailAddress("jjones@somewhereelse.org");     try {         // The equals implementation on Person should         // always return true when a Person is compared         // to itself         assertTrue(mPerson.equals(mPerson));     }     catch (Exception e) {         // Fail if the equals method generates any exceptions         fail("Unexpected exception: " + e.getMessage(  ));     } } 

In the test, we set up a Person object and use the equals( ) method to compare the Person with itself, asserting that this should always return true by using the assertTrue( ) method. We've wrapped the call to equals( ) with a try/catch block, and in the catch block, we fail the test with a descriptive message since the equals( ) method should never throw an exception under these test conditions.

18.3.4. Managing Test Fixtures

If you look at the tests on our TestPerson test case in Example 18-1 (and our additional testEquals( ) test just shown), you'll notice that, in each test, we're setting up a Person object to serve as the subject of our test. The baseline set of data used for a unit test is sometimes called the test fixture. In our TestPerson tests, we always use a single Person object as the test fixture. To support the use of test fixtures in unit tests, JUnit provides setUp( ) and tearDown( ) methods on TestCase. The setUp( ) method is invoked before each test method on a TestCase is run, and the tearDown( ) method is called after each test method. You can implement these methods to perform any initialization of data or other resources needed across your test methods and then do any cleanup afterward.

Example 18-2 shows an updated version of our TestPerson test case, using the setUp( ) and tearDown( ) methods to manage the fixture for the tests. Note that we've omitted some details here, like the constructors and some of the test methods. In this version of our test case, the test fixture is a Person object that we construct and initialize. In our tests, like the testSetName( ) method, we can now use the Person object as the subject of our tests. Notice that in our testSetName( ) method, we've opted to reset the first and last name attributes to local values. You might look on this as a waste of time (after all, the setUp( ) method has just initialized the Person object for us), but even with a test fixture in place, it might be useful to perform some redundant initialization in your test methods, such as we've done here, in order to localize expected results in the test method and make your tests more readable.

Example 18-2. TestPerson with a test fixture
 import junit.framework.TestCase;   public class TestPerson extends TestCase {     private Person mPerson = null;       . . .       /** Set up the test fixture before each test */     protected void setUp(  ) {         mPerson = new Person(  );         // Set the attributes to some defaults         mPerson.setFirstName("John");         mPerson.setLastName("Smith");     }     /** Clean up the test fixture after each test */     protected void tearDown(  ) {         mPerson = null;     }       /** Test the name accessors */     public void testSetName(  ) {         String fName = "Jane";         String lName = "Doe";         mPerson.setFirstName(fName);         mPerson.setLastName(lName);         assertNotNull(mPerson.getFirstName(  ));         assertTrue(mPerson.getFirstName(  ).equals(fName));         assertNotNull(mPerson.getLastName(  ));         assertTrue(mPerson.getLastName(  ).equals(lName));     }       /** Test the addition of email addresses */     public void testAddEmail(  ) {         String addr = "jsmith@anywhere.com";         mPerson.addEmailAddress(addr);         // There should be only a single email address on the person         assertEquals(mPerson.getEmailAddresses(  ).size(  ), 1);         // The email address should be the value we just added         String addrR = (String)mPerson.getEmailAddresses().iterator(  ).next(  );         assertNotNull(addrR);         assertEquals(addr, addrR);     }       . . . } 

It's important to remember that setUp( ) and tearDown( ) are called for every test method on a TestCase. This ensures that each test operates on a clean version of the fixture, but it can also cause unexpected performance issues if you do a lot of heavy lifting in these methods. If you're testing a data-caching engine, for example, and load up a large amount of data into the cache before each test method, it may take a very long time for all of your tests to complete. If you want to initialize a set of test data once for an entire suite of tests, you can do that in various ways, such as a static initializer on the TestCase subclass.

Once you've written a TestCase, you'll want to run its tests periodically in order to verify the correct behavior of the target code. That's the whole point of unit testing, after all. You can run your JUnit test in a number of ways. The most explicit way is to simply invoke a TestRunner with the full name of your TestCase class, and it will automatically find and execute all test methods on your TestCase (using introspection). The following command will run our TestPerson tests using the text-based TestRunner, for example:

 > java junit.textui.TestRunner com.oreilly.jent.people.TestPerson ... Time: 0.021   OK (3 tests) 

Notice that the text-based TestRunner prints a dot (.) for each successful test it runs and prints a summary of the results. If we ran the same TestCase using the Swing-based TestRunner, junit.swingui.TestRunner, we'd see the results shown in Figure 18-1.

Figure 18-1. TestPerson executed through the Swing-based TestRunner


18.3.5. Organizing Tests Using TestSuites

Using a TestRunner to run each of your TestCases is not very practical in situations in which you are testing a system with many components and therefore many, many TestCase implementations to manage. Also, you may want to have better control over which specific test methods are executed and in what order these tests run. Further, you may want to organize groups of tests into categories that run separately and are managed by different subsets of your development team. There may be a collection of unit tests for each component of a large enterprise system, for example, and the teams responsible for each system component may write and manage the corresponding unit tests. Another set of unit tests might perform specific tests related to integrating these separate components and might run during integration testing prior to a full system release.

JUnit supports organization of tests in these ways through the TestSuite class. As mentioned earlier, a TestRunner uses a TestSuite to run all of its tests. If you don't provide one, it simply wraps the TestCase with a TestSuite and then operates on the new suite.

JUnit offers an abundance of flexibility in terms of how you can organize your tests into suites. Starting at the most basic level, you can wrap any TestCase with a TestSuite by using the TestCase as a constructor argument. This example shows how we could create a TestSuite that includes all of the tests on our TestPerson class:

 TestSuite suite = new TestSuite(TestPerson.class); 

We can also specify that a TestSuite should include only a specific subset of the tests on a TestCase. Every TestCase has a constructor that takes a single String argument, interpreted as the name of a test method to be executed. If a TestCase is constructed with a method name this way and added to a TestSuite, only that test method will be run on the TestCase when the TestSuite is run through a TestRunner. If, for example, we wanted to run only our testSetName( ) test on TestPerson, we'd construct a TestSuite this way:

 TestSuite suite = new TestSuite(new TestPerson("testSetName")); 

You can also compose suites with a mix of individual tests and entire TestCase instances, using the addTest( ) and addTestSuite( ) methods on TestSuite:

 TestSuite suite = new TestSuite(  ); // Add the testSetName test from TestPerson suite.addTest(new TestPerson("testSetName")); // Add all the tests on the TestPersonDAO class suite.addTestSuite(TestPersonDAO.class); 

As mentioned earlier, a TestRunner always wants to operate on a TestSuite. When you pass a TestCase to a TestRunner, the runner first checks to see if the TestCase has a static suite( ) method on it. If it does, the runner invokes this method and uses the returned suite as the set of tests to run. If the TestCase has no suite( ) method, the runner constructs its own suite containing all of the tests found on the TestCase. If we wanted to limit the testing of Person to include only the testSetName( ) and testAddEmail( ) tests, for example, we could add the following suite( ) method to TestPerson:

 static public Test suite(  ) {     TestSuite suite = new TestSuite(  );     suite.addTest(new TestPerson("testSetName"));     suite.addTest(new TestPerson("testAddEmail"));     return suite; } 

When any TestRunner is used with TestPerson, the runner calls this suite( ) method and gets back a suite containing only these two test methods.

You can use the suite( ) method to define aggregate TestCases that include groups of tests for your system. For example, if we had two TestCase subclasses, TestPerson and TestPersonDAO, that tested the basic model and data access features of the PeopleFinder system, we could define a simple TestCase that included them to make it easier for us to drive all tests related to this subset of our system, as shown in Example 18-3.

Example 18-3. Utility TestCase that aggregates other TestCases into a TestSuite
 import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestSuite;   public class AllModelTests extends TestCase {     public static Test suite(  ) {         TestSuite suite = new TestSuite("Tests for the PeopleFinder model");         // First test the Person data bean         suite.addTestSuite(TestPerson.class);         // Test the Person DAO, which is dependent on the Person bean         suite.addTestSuite(TestPersonDAO.class);         return suite;     } } 

We could then test all of our model-related unit tests by simply invoking this utility TestCase with a TestRunner, like so:

 > java junit.textui.TestRunner com.oreilly.jent.people.AllModelTests 

TestSuites can also be used to impose a certain order on the sequence of tests. In our previous example, we would likely want to run the tests on our Person bean first, followed by the tests on our PersonDAO since the PersonDAO has a functional dependency on the Person bean. If the Person bean tests fail, it is likely that the PersonDAO tests will fail as well (or, at a minimum, the results of the PersonDAO tests won't mean that much until we're sure that the Person bean works properly).



Java Enterprise in a Nutshell
Java Enterprise in a Nutshell (In a Nutshell (OReilly))
ISBN: 0596101422
EAN: 2147483647
Year: 2004
Pages: 269

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net