|
Close Window |
Unit tests give us the confidence to change applications, even legacy applications that we didn't write ourselves. To avoid the drudgery of writing a test case for every edge and boundary, we can let JUnit Factory generate a large set of characterization tests for us. If we need to explore an existing code base, characterization tests can act as a learning guide by documenting what the code actually does. If we need to change existing code, characterization tests act as critical "change detectors" that help us avoid unforeseen side effects.
Tests Give Us the Confidence To Change
We often inherit applications that we didn't write. The application works and the users seem happy, at least for now. But, we still need to be ready to maintain the application as soon as the next bug report or enhancement request comes down the pike.
An excellent way to explore an unfamiliar application is to create unit tests. Tests not only alert us when something is wrong; tests demonstrate how the code actually works. Of course, creating unit tests for existing code isn't trivial, especially when it comes to code we didn't write.
A case in point is the Spring JPetStore. JPetStore is a production-quality rendition of Sun's infamous PetStore application, originally created by Clinton Begin to showcase using iBATIS with Struts. The Spring rendition added a Spring MVC implementation and refactored the business layer for Spring. Surprisingly, the Spring JPetStore doesn't include a suite of unit tests against the business logic. The Spring JPetStore welcome page is shown in Figure 1.
We'd like to bring the Spring JPetstore up-to-date with the latest version of iBATIS and add a Struts 2 implementation. But before starting on that, we'd like to add a set of unit tests for the back-end. With a good set of tests on board, we can make changes and not worry that something will break without notice. Kent Beck, widely recognized as the father of eXtreme Programming and JUnit, tells us that we should "test until fear turns to boredom." Good advice, but being lazy, we'd rather not create all the tests by hand.
Avoid Drudgery with Generated JUnit Tests
JUnit Factory (www.junitfactory.com, shown in Figure 2) is a free test generation service offered by Agitar Software that can generate tests by analyzing source code. JUnit Factory is powered by the same technology that's behind AgitarOne, Agitar's comprehensive, server-based unit testing solution for Java.
We can submit code to the JUnit Factory service with either a Web browser or an Eclipse plug-in. JUnit Factory analyzes the code and generates unit tests based on how the code behaves.
In this article, we'll use JUnit Factory to create a suite of standard JUnit tests for the Spring JPetstore business layer, so that we can refactor or extend the business logic with confidence. As we change the JPetStore code, we'll run the tests to see whether we have broken any of the code's existing behavior. As we go along, we can update the tests by hand, generate new tests, or both.
Say Hello To Generated JUnit Tests
Before we study the JUnit tests for JPetStore, let's take a look at the JUnit tests generated for two simple examples. Of course, the first example is "Hello World!" shown in Listing 1.
A reasonable unit test for HelloWorld, as generated by JUnit Factory, is show in Listing 2.
Nothing fancy, but it shows us exactly what the application does: it says Hello World!
From now on, whenever we change the HelloWorld class, we can run testSayHello() to see if anything breaks. If a test does fail, it's a signal that a method doesn't work the same way anymore. Sometimes, a failing test means that our change had unexpected consequences and by fixing one bug we've accidentally created another. (So, it's back to the drawing board!)
Other times, a failing test means that we've changed our expectations of how the code should behave, and the failing test is now obsolete. With conventional JUnit tests, an API change means we have to update any outdated tests. With a tool like JUnit Factory, instead of rewriting obsolete characterization tests, we can regenerate those tests instead, and create a new baseline.
What about something a little more challenging like the calculation of a leap year shown in Listing 3?
Given a method with various output possibilities, JUnit Factory will generate a set of unit tests such as those shown in Listing 4.
To do unit testing right we need a lot of tests - more tests than most of us would like to write. Along with the main "success" path, we should also check boundary and edge conditions: what happens if the year is zero, or if the year is divisible by 4, or if the year is exactly 4, and so on.
In Listing 4, we can see that JUnit Factory was able to generate a decent array of tests for the LeapYear class - again far more unit tests than most of us would have the patience to devise and code, especially for code we didn't write ourselves.
Agitar calls the tests it generates "characterization tests" to emphasize that these JUnit tests reflect what the code actually does.
The generated tests always work if you run them against the original code under test. When run as regression tests, characterization tests serve as API change detectors: if we change an application and find that one or more characterization tests fail then we should consider the change "on notice." We might need to rework the change, or we may have to regenerate the tests. But, either way, characterization tests red-flag changes in public behavior.
Generate JUnit Tests for JPetStore
The JUnit Factory site is fun, but it only works with classes that have no dependencies. To generate tests for an entire application such as JPetStore, we need to use the JUnit Factory Eclipse plug-in, which is also free.
JPetStore is a shopping cart application. Visitors can browse our inventory, add items to a virtual shopping cart, and place an order for the items when ready.
To generate tests for JPetStore, we should first isolate which classes we'd like to test. Unit testing is best suited to plain Java objects that handle business logic. Other objects, including data access objects, can be tested too, but that requires more setup.
Like many applications, the core of JPetStore is represented as a set of business objects that represent entities in the problem domain. In our case, the business entities are client accounts, inventory products, product categories, inventory items, shopping carts, sale orders, and sale items. The JPetStore business objects are conveniently gathered into a package named domain.
Let's say that we've installed the JUnit Factory plug-in and created a Java project. To generate characterization tests, all we have to do is right-click in Eclipse's Package Explorer view then select Agitar/Generate Tests. The plug-in submits the classes to the JUnit Factory Web site and within a few minutes the JUnit Factory will generate and return over 100 tests, organized in separate test suites for each of the eight classes.
If we run the characterization tests against the original classes, the classes will pass with flying colors (well, green), as shown in Figure 3. This is to be expected: characterization tests will always work against the unmodified code under test, because the tests are based on what the code actually does-good, bad, or indifferent.
Since the JPetStore Domain is designed with JavaBeans, many of these tests are simple exercises of getters and setters. Other more interesting tests peer into the business logic as Listing 5 shows.
JUnit Factory realized that if it sets the ListPrice on a single CartItem and then calls getTotalPrice(), it will get a value equal to ListPrice. In other words, if there's only one item in the shopping cart, the total price for everything in the cart is the same as the price of that one item.
Very Interesting - But a Bug!
Listing 6 shows another "interesting" characterization test.
The test is telling us that if the line items are set to null, the system will throw a runtime NullPointerException. Remember, a characterization test documents what the code under test actually does: JUnit Factory isn't offering an opinion of the correctness of that behavior.
Let's look at Listing 7, which shows bits of the code under test.
Class Order instantiates lineItems then initializes it to an empty ArrayList(). As written, other parts of the class simply assume that lineItems will never be null. But since there is a public setLineItems method, the non-null assumption seems rash. Being prudent programmers, we decide to fix this latent bug before it crawls out of the woodwork: the fix is to apply "lazy instantiation" to the lineItems field, as Listing 8 shows.
To finish the job, we make sure that the class is calling its own getter method throughout rather than accessing the field directly. (A good practice under any circumstances!)
If we clear the tests (by deleting the agitar/test folder) and regenerate, JUnit Factory will return us one fewer test.
Looking back, we see that JUnit Factory uncovered the missing null-guard just by analyzing the code. It also discovered the relationship between list price of an item and the total price of an order, all without being told. Hmmm...I wonder what it could do if we could give JUnit Factory a clue as to how the domain objects are supposed to work?
Generate Better Tests with Realistic Test Data
In
the context of the JPetStore application, Items are magical objects
that appear fully formed from the database (via Data Access Objects).
JPetStore is a consumer-facing shopping cart application: the
application never creates an Item itself. All it does is wrap Items in
CartItems, which go into the shopping Cart.
We could generate more useful tests by providing a realistic set of Item objects rather than let JUnit Factory create them willy-nilly. To create realistic objects, we can write a Test Data Helper.
Essentially, by implementing the TestHelper "marker" interface, we create a simple factory that provides well-formed objects. When JUnit Factory needs an instance of a particular class, it looks to see if there are any helper methods available that return objects of that class. If any of these helper objects will work for the test JUnit Factory is trying to generate, it will give the helper objects priority over test objects it creates from scratch.
Using our inside knowledge of what kind of items appear in our database, we can write a TestHelpers class that provides realistic Item objects for our tests to use, as Listing 9 shows.
With TestHelpers in place, the generated tests will reference our factory methods, instead of creating objects from whole cloth. Listing 10 is an example.
Instead of making something up, the testToString method calls the toString method on one of our TestHelper classes. The toString form is a concatenation of the item ID and the product ID. Just by glancing at the generated test, we can see that it is characterizing the code correctly - thanks in part to our TestHelpers class that provided realistic data for the tests.
Extend the Business Logic with Confidence
Now that we have a set of characterization tests, we can get to work on the business end of the application.
•
We can study each test suite alongside the code under test to help us
better understand how the legacy application actually works.
• Along the way, if we note "suspicious" or "unexpected" behavior
in a test, we can dig deeper to see if there's a latent bug that may
complicate maintenance.
• We can extend and refactor the application with confidence, since
our characterization tests will alert us of unintended consequences.
• When we do decide to change the behavior of the application, we
can regenerate the characterization tests as a new baseline for future
changes.
Characterization tests are unit tests, but they should not be all the unit tests. We should still write a smaller set of custom tests, particularly as we create new classes and methods. But, with JUnit Factory in our tool belt, we can focus our time on custom tests that exercise key features, while the tool generates the bulk of the boring, but valuable, characterization tests.
© 2008 SYS-CON Media Inc.