Testing from the Inside Out

The concept of a test-driven approach to development is extremely attractive if the full benefits of the paradigm can be realized. An exhaustive set of automated tests provides the safety net necessary for rapidly accommodating change on the project. In addition, it raises software quality, which in turn reduces the number of defects detected in formal systems testing. All of these benefits combine to facilitate a rapid development process.

While the concept of test-driven development is sound, the practicalities of implementing a suite of automated unit tests at the level required represents a sizeable technical challenge for the project team.

Writing valid unit tests is not a trivial task. In some cases, writing the test can prove a greater technical challenge than writing the class under test.

One of the greatest difficulties lies in isolating the class under test. Within the boundaries of a system, an object collaborates with other objects in order to execute specific functionality. For example, an object may require a number of domain objects returned from the persistence layer. The state of these domain objects determines the behavior of the class under test.

Designing a test that provides a class with all the information it needs to perform a specific operation can be an involved process, as dependencies between components make the testing of objects in isolation problematic. Good design practice that sees component coupling carried out through strongly typed interfaces can help ease the burden of writing unit tests. However, pulling an object out from a nest of collaborating instances is far from a trivial exercise in even the most well-designed system. Moreover, complete test coverage requires that a number of different scenarios be run. This involves setting the state of collaborating objects for each test scenario.

Object isolation is not the only issue with a test-driven approach. If we stick rigidly to the rule that each class is fully tested before proceeding to the next class to be developed, we are forced to adopt a bottom-up approach, whereby all foundation classes must be implemented ahead of any classes relying on those foundation services.

A bottom-up approach is not always the most practical approach. Project scheduling may dictate that classes in upper layers of the architecture must be implemented in parallel with classes from the lower layers. This raises the question of how to test an object when the classes upon which it is dependent have yet to be written, a problem inherent to even traditional development approaches.

One solution to the problem of how to test objects in isolation and undertake a top-down approach is to use mock objects, familiarly known as mocks.

What Is a Mock?

A mock object is best described as a dummy, or stub, implementation of an actual class. Mocks take the place of the real objects a class under test relies upon. Thus, if the class under test requires a domain object in a particular state for a specific test scenario, then you can substitute the domain object with a mock. Likewise, if the domain object has yet to be implemented, a mock can take its place.

The term mock was first coined in a notable paper by Tim Mackinnon, Steve Freeman, and Philip Craig, Endo-Testing: Unit Testing with Mock Objects, [Mackinnon, 2000]. The authors invented the term endo-testing as a play on endoscopic surgery, a process that enables the surgeon to work on the patient without having to make a large incision to allow access.

Mocks may sound very similar to test stubs in that they stub out the real code. Like test stubs, a mock can be configured to return a specific result in order to conduct a test scenario. This approach makes it easier to simulate events that are difficult to produce in a real environment, for example, a database connection being unavailable or an exception being thrown from a method call.

The comparison with test stubs is valid, but mocks go beyond what is offered by the typical test stub. Units test operate on the public- and package-level members of a class. Although they should exercise all execution paths within the code and test all boundary conditions, they do not interact directly with the internals of the class. However, using mocks, the internals of a class can be inspected as part of the test; hence the reference to endoscopic surgerymocks enable testing from the inside.

For the class under test, we can predict what calls we expect to be made on a mock object for a given test scenario. The mock object can record these calls. If an expected call fails to be made or the calls are made in the wrong order, then the mock can elect to fail the test. Thus, mocks allow a form of white-box testing to be undertaken as part of the unit testing process.

White-box testing is testing inside the methods of a class. Unlike black-box testing, which tests what a class does, white-box testing focuses on how a class performs its tasks. It is usually an invasive process and requires the class to be implemented in such as way that the internals can be examined while executing. Mocks offer a mechanism to inspect the internals of the class without having to structure the code in a manner that directly supports white-box testing.

Working with Mocks

Mocks require that we follow good design practices and architect our system around the use of interfaces. Divorcing an object's implementation from its interface is sound software engineering. It also facilitates the use of mock objects because the approach enables mocks to be easily substituted in place of the real object.

When it comes to writing mocks for a unit test, you can write the mock from scratch, implementing all the methods on the interface as needed for the test. This approach is labor-intensive and means the amount of code dedicated to testing the system is prone to bloating in size. Thankfully, a number of frameworks are available that can help reduce the effort involved in producing mocks. We consider these next.

Mock Flavors

Mocks take on the interface they are mocking. With an interface defining the shape of the mock, the mock object is another ideal candidate for code generation.

Although numerous options exist for generating mock objects, there are essentially two distinct approaches: static mocks and dynamic mocks.

Static mocks can be either handwritten or the output of a mock object code generator. Several code generators for mock objects are available; MockCreator and MockMaker are two notable examples. Each of these generators creates a mock object for a given interface. Optional Eclipse plug-ins are available for each generator.

For more information on MockCreator, see http://mockcreator.sourceforge.net. To obtain MockMaker, visit the site at http://www.mockmaker.org. Both code generators are freely available. In addition, XDoclet provides a mock generator, <mockdoclet>, which generates mock objects from the @mock.generate tag.

Chapter 6 covers the use of XDoclet.

By now, you should be well versed in the use of code generators and how XDoclet attributes can drive code generation. We now look at dynamic mock objects, which typically rely on Java reflection to work their magic.

Dyna-Mocks to the Rescue

As with static mock generators, a number of freely available dynamic mock implementations exist. Again, we have two notable examples: jMock and EasyMock. Both enjoy a level of popularity among developers. and each provides a reasonable level of documentation. In selecting between the two, your best option is to perform your own bake-off and determine which best suits the needs of your particular project.

JMock is maintained under the Codehaus project. so see http://jmock.codehaus.org. For EasyMock, pay a visit to http://www.easymock.org.

To show how mocks can assist in the process of unit testing and to demonstrate how dynamic mocks work, we go through an example unit test that uses a dynamic mock as the collaborating object for the class under test. The example uses EasyMock for defining the mock object.

The objective of the example test is to validate that the class under test calls the correct methods on the collaborating object. The example is based on two classes: the class under test, Invoice, and the collaborating object, which is an implementation of the interface ICustomer. Listing 14-4 shows the ICustomer interface.

Listing 14-4. The ICustomer Interface
 public interface ICustomer {   /**    * Customer's discount entitlement    */   public abstract double discountRate();   /**    * Customer's interest on credit sales    */   public abstract double creditRate(); } // ICustomer 

Listing 14-5 has the implementation of the Invoice class.

Listing 14-5. The Invoice Class under Test
 public class Invoice {   private double    invoiceAmount;   private ICustomer customer;   /**    * Associate the invoice with a customer    *    */   public Invoice(ICustomer customer) {     this.customer = customer;   }   /**    * Set the invoice amount    */   public void setInvoiceAmount(double invoiceAmount) {     this.invoiceAmount = invoiceAmount;   }   /**    * Calculate the customer's discount for the invoice amount    */   public double discount() {     return invoiceAmount * (customer.creditRate() / 100);   } } // Invoice 

The Invoice class doesn't do much, but a mistake has still been made. The discount() method returns the value of the discount the customer receives off the total invoice amount. The calculation is based on the customer's special discount rate. In this system, loyal customers get a better discount. A customer's discount percentage is returned from the method discountRate() by the object implementing the ICustomer interface. Unfortunately, in this case, the discount() method on the Invoice class has been incorrectly implemented by using the customer's credit rate in place of the discount rate.

It's a silly error, but problems of this type can easily arise if care isn't taken when using code-editor productivity features like code assist or code completion. Here the developer has inadvertently selected the wrong method from the list. Code assist is an invaluable editor feature, but you have to be careful not to get too lazy.

To detect this type of error, a test is required that confirms the correct methods are being called on the collaborating objects, in this case ICustomer. This is easily done with mocks.

Listing 14-6 gives the test case for the test, using a dynamic mock object in place of the implementation for the ICustomer interface.

Listing 14-6. The InvoiceTest Unit Test Case
 import junit.framework.TestCase; import org.easymock.MockControl; public class InvoiceTest extends TestCase {   protected void setUp() throws Exception {     super.setUp();     // Create mock based on interface     //     control = MockControl.createControl(ICustomer.class);     customerMock = (ICustomer) control.getMock();     // Prepare the class under test     //     invoice = new Invoice(customerMock);     invoice.setInvoiceAmount(200.0);   }   public final void testDiscount() {     // Configure the mock ready for the test     //     customerMock.discountRate();     control.setReturnValue(10.0);     // Place mock in the active state     //     control.replay();     // Run the test     //     assertEquals(20.0, invoice.discount(), 0.0);     // Ensure test class calls discountRate() method     //     control.verify();   }   private MockControl control;   private ICustomer   customerMock;   private Invoice     invoice; } // InvoiceTest 

The setUp() method of the test case is where the dynamic mock is created. Using Easy-Mock, two classes are created: an instance of MockControl, which takes the type of interface to be mocked as a parameter and is used for controlling the mock object, and the mock object itself, which is returned from the MockControl instance with a call to the factory method get-Control().

The mock returned from the MockControl exposes the ICustomer interface, so we can substitute the mock for the real object. MockControl achieves this feat of impersonation by using java.lang.reflect.Proxy under the covers.

The final act of the setUp() method is to create an instance of Invoice for using in the test. The mock is passed to the Invoice object via its constructor. The Invoice instance is then seeded with an invoice amount for the test. This value is used for checking against our expected results.

The final steps for setting up the test are completed within the testDiscount() method on the TestCase. At this stage, the newly created mock is in record mode. In this state, the methods that must be invoked on the mock when the test is run can be specified. EasyMock enables this information to be specified in detail. You can specify the order of calls, the number of times each call should be made, and the expected parameters. Refer to the EasyMock documentation for a full list of its capabilities.

The test in the example isn't overly complex. Next, we ensure that the correct method is invoked on the collaborating object and that the class under test returns the expected result.

For defining the expected method invocation, the required method is called on the mock object with the mock in the record state. In this case, the method is discountRate().

The next step is to set the result returned by the mock when the class under test invokes the method. This is achieved by calling setReturnValue() on the MockControl instance.

Now that the mock is configured, it is placed in the active state with a call to replay() on the MockControl.

A call to discount() on the Invoice object initiates the test. The called method is wrapped in a JUnit assertEquals so the expected results can be confirmed. The final call to verify() on the MockControl instructs the mock controller to assert if the methods specified when the mock was in the record state are not invoked. All that remains to do is run the test.

Here is the error reported when the test case is run.

 junit.framework.AssertionFailedError:   Unexpected method call creditRate():     creditRate(): expected: 0, actual: 1 

The mock asserts during the execution of the test that an unexpected method, creditRate(), has been called. We've found our bug. The troublesome code can be corrected and the test rerun to achieve the green bar indicating a successful test.

Based on the example, you can see that mocks add another dimension to the type of unit testing you can perform. Not only can the mock stand in for the real collaborating object, as is possible with a traditional test stub, it can also validate the class under test from the inside. This all leads to a rigorous testing process and hence higher quality code.

Choosing Between Static and Dynamic Mocks

Dynamic mocks are attractivethey offer the ability to create powerful test stubs at runtime. All the code relating to a test can be maintained within the test case itself, avoiding the need to support further test code for the implementation of the mock.

Despite these benefits, mocks are not suited to all test scenarios. The example shown cannot be used to test an EJB component through its remote interface, as the dynamic proxy cannot be marshaled as part of the call. Moreover, as has already been stated, testing is not a trivial undertaking, and it is likely a combination of static and dynamic mocks will make up the overall test suite.

A general heuristic to follow when working with mocks is to use dynamic mocks where possible for their convenience and setup speed. Dynamic mocks help keep the size of the code base down and can be localized to a single test. For situations in which this cannot be achieved, look to generate the mock, and if all else fails, simply write your own implementation. Static mocks can be used to test Enterprise JavaBeans, and the strong typing of a static interface can be an advantage because it enables the compiler to detect errors in the test suite.

    Rapid J2EE Development. An Adaptive Foundation for Enterprise Applications
    Rapid J2EEв„ў Development: An Adaptive Foundation for Enterprise Applications
    ISBN: 0131472208
    EAN: 2147483647
    Year: 2005
    Pages: 159
    Authors: Alan Monnox

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