JUnit Factory Part 1: Generating Tests (page 2)

« Return to JUnit Factory Part 1: Generating Tests (page 1)

Methods with complex parameters

JUnit Factory easily is able to try different combinations of primitive parameter types for a method under test. But, what about method parameters that are classes?

JUnit Factory uses a number of strategies to figure out how to construct objects to pass as arguments. The simplest is reflection. By reflecting the constructor of the parameter type, JUnit Factory attempts to learn how to instantiate its own instances.

It’s not always trivial, however, to figure out how to instantiate a complex class in a valid state – or enough multiple states to attain complete execution coverage of a method. Therefore, JUnit Factory also inspects the rest of the project code – including any of your own hand written JUnit tests – to see how instances are created elsewhere.

To see an example of how JUnit Factory handles complex parameter types, let’s add two complementary methods to our CreditCard class: one for recording purchases, and one for recording payments:

public void makePurchase( Purchase purchase ) throws CreditCardException

{

validateBalance( balance + purchase.getAmount() );

balance = balance + purchase.getAmount();

}

public void makePayment( double amount ) throws CreditCardException

{

if (amount <= 0)

{

throw new CreditCardException(“Payment amount must be positive”);

}

validateBalance( balance – amount );

balance = balance – amount;

}

The makePurchase() method first ensures the balance would still be valid after applying the purchase amount (for example, the new balance may not exceed the credit limit). Notice it takes a complex parameter of type Purchase.

The Purchase class encapsulates both a purchase date and a purchase amount:

public class Purchase

{

private Date transactionDate;

private double amount;

public Purchase( Date transactionDate, double amount )

throws CreditCardException

{

if (amount <= 0)

{

throw new CreditCardException(“Purchase amount must be positive”);

}

this.transactionDate = transactionDate;

this.amount = amount;

}

public Date getTransactionDate()

{

return transactionDate;

}

public double getAmount()

{

return amount;

}

}

Naturally, JUnit Factory also generates characterization tests for Purchase, too, and you can see them by downloading the sample code project. For now, however, let’s see how the Purchase class affects the test case generated for CreditCard.

JUnit Factory generated three tests for makePurchase(). This is one of them:

public void testMakePurchase() throws Throwable {

CreditCard creditCard = new CreditCard(“2298 9812 4566 1184”,

100.0, 0.0);

creditCard.makePurchase(new Purchase(new Date(100L), 100.0));

assertEquals(“creditCard.getBalance()”,

100.0,

creditCard.getBalance(), 1.0E-6);

}

Here, a CreditCard is instantiated with a credit limit of 100.0 and an opening balance of 0.0. A purchase of 100.0 is applied to the object, and the new balance is asserted to be 100.0. Notice JUnit Factory figured out how to instantiate a Purchase instance.

The ability to instantiate complex types in order to increase test execution coverage isn’t just used when the method under test requires a complex object. This ability is used wherever a complex object is required. For example, the makePayment() method shown earlier requires only a primitive double as a parameter, but JUnit Factory generates a test that looks like this:

public void testMakePayment2() throws Throwable {

CreditCard creditCard = new CreditCard(“2298 9812 4566 1184”,

100.0, 0.0);

creditCard.makePurchase(new Purchase(new Date(100L), 100.0));

creditCard.makePayment(100.0);

assertEquals(“creditCard.getBalance()”,

0.0,

creditCard.getBalance(),

1.0E-6);

}

This makePayment() test leverages the behavior of makePurchase() in order to modifying the credit card balance. Once again, JUnit Factory successfully figured out how to instantiate a Purchase where needed.

All of these new tests pass. So, what?

What good is it if JUnit Factory only generates tests that always pass? How is that going to help you find bugs? After all, if our CreditCard class contains any bugs, the generated tests are simply going to capture and reflect that incorrect behavior.

Remember that JUnit Factory generates characterization tests and not functional tests. It may help to review “Characterization Tests: How To Deal With Legacy Java Code”. What we now have is a safety net against regressions. These tests don’t fail today. But, if they fail in the future, we’ll know we changed the behavior of the CreditCard class. Whether that’s an intentional change or a bug must be determined by the developer.

Next, we’ll take a look at what happens when we make a simple requirements change to CreditCard and how we’ll use our characterization tests to guard against regressions.

Go to “JUnit Factory Part 2: Finding Regressions” »