Introduction to Test Driven Development and JUnit
Filed under: agile process
There are 0 comments on this article.
Introduction
Test Driven Development (TDD) is a popular programming practice used by developers to attain high-levels of quality in their code. The TDD method consists principally of planning and writing unit tests first. For the implementation of some new piece of functionality, TDD can simply be stated as follows:
- you write a test class, compile and execute it
- the test class fails
- you write the implementation class (e.g. a Java class), compile and execute it just enough to make the test class pass
- you refactor the implementation class as necessary
As an example, suppose you were creating a new Java class to model a customer’s bank account and that the account would ultimately hold data and have methods to access this data as follows:
// private data private String id; // account identifier private String type; // account type private BigDecimal balance; // account balance // public methods public Account(); public Account(String accountid); public String getId(); public String getType(); public BigDecimal getBalance(); public void setId(String id); public void setType(String type); public void setBalance(BigDecimal balance); public BigDecimal deposit(BigDecimal amount); public BigDecimal withdraw(BigDecimal amount);
Before you even wrote the code for this class, it would be good practice to work out how it would be exercised, identifying the successful and unsuccessful boundary and error conditions. For example:
- what would happen if I requested information on an account that did not exist?
- what would happen if I set the account balance to both positive and negative values?
- what would happen if I tried to withdraw more cash than the account currently held?
These conditions can be coded using a unit testing framework like JUnit (for Java code) using what are called assertions. To implement TDD, you would therefore write a JUnit test class to instantiate the account class and exercise its (as yet unimplemented) methods. At certain points in the code you assert what should happen, based on the input you provide. As an example, the starting point for the JUnit test class (in this case called TestAccount.java) would look something like this:
public class TestAccount extends TestCase {
private Account a1;
public TestAccount(String arg0) {
super(arg0);
}
protected void setUp() {
a1 = new Account("101-1001");
a1.setType("Current");
a1.setBalance(new BigDecimal("100.25"));
}
public void testGetters() {
assertTrue(!a1.equals(null));
BigDecimal balance = new BigDecimal("100.25");
assertEquals(a1.getId(), "101-1001");
assertEquals(a1.getType(), "Current");
assertEquals(a1.getBalance(), balance);
a1.setId("101-1002");
assertEquals(a1.getId(), "101-1002");
}
public void testDeposit() {<
BigDecimal balance = new BigDecimal("150.50");
assertEquals(a1.deposit(new BigDecimal("50.25")), balance);
}
public void testWithdraw() {
BigDecimal balance = new BigDecimal("70.01");
try {
assertEquals(a1.withdraw(new BigDecimal("30.24")), balance);
} catch (InsufficientFundsException ex) {
ex.printStackTrace();
}
try {
// withdraw too much
a1.withdraw(new BigDecimal("10000.00"));
fail("withdraw hasn't caused an exception when it should");
}
catch (InsufficientFundsException ex) { }
}
}
This JUnit test class has a number of test cases identified by each method that starts with the word test (for example testDeposit), it also has an additional method called setUp which JUnit invokes before each method to ensure a stable test environment (it is also possible to specify a tearDown method to cleanup an environment). Of particular interest is the testWithdraw method which implements a test to withdraw more than the account contains (on lines 37 to 40). In this scenario we want to make sure that the InsufficientFundsException is called, if not we have an error in our code and the JUnit fail method is used to fail the test case.
Obviously compiling and executing this test class on its own, without the implementation class would fail, so next an implementation class (in this case called Account.java) would be developed to make these test cases pass as follows:
public class Account implements Comparable, Serializable {
// The account business data
private String id;
private String type;
private BigDecimal balance;
public Account() { super(); }
public Account(String accountid) {
setId(accountid);
setBalance( new BigDecimal(0.00));
}
// getters
public String getId() { return id; }
public String getType() { return type; }
public java.math.BigDecimal getBalance() { return balance; }
// setters
public void setId(String id) { this.id = id; }
public void setType(String type) { this.type = type; }
public void setBalance(java.math.BigDecimal balance) { this.balance = balance; }
// business methods
public BigDecimal deposit(BigDecimal amount) {
setBalance(getBalance().add(amount));
return getBalance();
}
public BigDecimal withdraw(BigDecimal amount) throws InsufficientFundsException {
if (getBalance().compareTo(amount) == -1)
throw new InsufficientFundsException("Insufficient funds for withdrawal");
setBalance(getBalance().subtract(amount) );
return getBalance();
}
}
Over time you would obviously be asked to add new methods, in which case again you would update the test class first, it would fail and you would then write the implementation class. Also over time you might need to refactor the implementation class, maybe because you want to make the code more readable or need to optimize it for performance. In lots of teams, developers are often afraid of optimization because they have brittle code and it might introduce errors. However with TDD you always have the test classes - these can be executed to check that you have not violated the interface or introduced any unnecessary side affect.
Test Driven Driven can prove very effective during defect fixing. For example, if a new defect has been found, as a developer your first step should be to ascertain whether you can write a unit test to expose the defect. Although this might not always be possible, if it is, then you now have the opportunity to embed it in your unit test suite and ensure that this type of defect does not appears again.
In practice Test Driven Development is often combined with Continuous Integration and can be very powerful and effective way of incrementally delivering software in dynamic environments. There are many tools that can be used to support TDD other than JUnit, most of which are in the nUnit family. For example HTTPUnit can execute functional tests on a set of web pages using the HTTP protocol, whilst SQLUnit can execute tests on database stored procedures. Where applications need to be executed in some form of runtime container (e.g. Enterprise Java Beans) then additional tools are often used to support TDD. For example mock objects can be created that run outside of the container using tools like mockEJB or the application can be deployed into the container and tools like Cactus used to execute them in a more "integration" like environment.
For more background information on Test Driven Development see Kent Beck's book. For more detail on how to construct JUnit tests and test suites see the book by Andy Hunt and Dave Thomas.
