Lecture 26: Test Driven Design
Credit where Credit is Due
- Some of the material for this lecture is taken from “Test-Driven Development” by Kent Beck; as such some material is copyright © Addison Wesley, 2003
- In addition, some material for this lecture is taken from “Agile Software Development: Principles, Patterns, and Practices” by Robert C. Martin; as such some materials is copyright © Pearson Education, Inc., 2003
Goals for this Lecture
- Introduce the concept of Test-Driven Design
- Present several examples
- Review one person's experience with Test Driven Design
- Look (briefly) at testing frameworks
Test-Driven Design (TDD)
- The idea is simple
- No production code is written except to make a failing test pass
- Implication
- You have to write test cases before you write code
Writing Test Cases First
- When you first write a test case, the code you are testing does not exist!
- Test fails because the test can't compile!
- Write skeleton code for the objects referenced in the test case: test now compiles but still fails
- Write the simplest code that will make the test case pass
Example (IV)
- But, this code is not very useful!
- Lets add a new test case
public class TestGame extends TestCase {
public void testOneThrow() {
Game g = new Game();
g.addThrow(5);
assertEquals(5, g.getScore());
}
public void testTwoThrows() {
Game g = new Game();
g.addThrow(5);
g.addThrow(4);
assertEquals(9, g.getScore());
}
}
The first test case passes, but the second test case fails (because 9 ≠ 5)
Example (VI)
- Alas good times never last! Our next test case:
- Lets add a new test case
public class TestGame extends TestCase {
public void testOneThrow() {
Game g = new Game();
g.addThrow(5);
assertEquals(5, g.getScore());
}
public void testTwoThrows() {
Game g = new Game();
g.addThrow(5);
g.addThrow(4);
assertEquals(9, g.getScore());
}
public void testSimpleSpare() {
Game g = new Game();
g.addThrow(3);
g.addThrow(7);
g.addThrow(3);
assertEquals(13, g.scoreForFrame(1));
assertEquals(16, g.getScore());
}
}
We're back to the code not compiling. We'll need to change the Game class to add the scoreForFrame() method
Then, we would need to add the simplest code that made both assertions pass
Ideas?
TDD Life Cycle (I)
- The life cycle of test-driven design is:
- Quickly add a test
- Run all tests and see the new one fail
- Make a simple change
- Run all tests and see them all pass
- Refactor to remove duplication
- This cycle is followed until you have achieved your goal (whatever that may be)
TDD Life Cycle (II)
- TDD is often performed in the presence of a testing framework, such as JUnit
- Within such frameworks
- failing tests are indicated with a “red bar”
- passing tests are indicated with a “green bar”
- As such TDD is sometimes described as
- “red bar/green bar/refactor”
- We will look at two testing frameworks at the end of this lecture
Background: Multi-Currency Money
- Lets design a system that deals with financial transactions with money that may be in different currencies
- For example, if we know the exchange rate between Swiss Francs to U.S. Dollars is 2 to 1, we can calculate expressions like
- 5 USD + 10 CHF = 10 USD
- 5 USD + 10 CHF = 20 CHF
Starting From Scratch
- Lets start developing such a system. How?
- TDD recommends writing a list of things we want to test
- This list can take any format; just keep it simple
- You then write a test case and work with it until it passes
- You then cross that test off your list and move on to another test
- Our first pass at the list will look like this:
- $5 + 10 CHF = $10 if rate is 2:1
- $5 * 2 = $10
What’s Next?
- We need to update our test list
- Our first test case revealed some things about Dollar that we will want to address
- We are representing the amount as an integer, which will make it difficult to represent values like 1.5 USD; how will we handle rounding of factional amounts?
- Dollar.amount is public; violates encapsulation
- What about side effects? (we first declared our variable as “five” but after we performed the multiplication it now equals “ten”)
Update Testing List
- The New List
- 5 USD + 10 CHF = 10 USD
- $5 * 2 = $10
- make “amount” private
- Dollar side-effects?
- Money rounding?
- Now, we need to fix the compile errors
- no class Dollar, no constructor, no method: times(), no field: amount
Dollar Class, v. 0.1
public class Dollar {
public Dollar(int amount) {
}
public void times(int multiplier) {
}
public int amount;
}
Now our test compiles, but fails
Too Slow?
- Note: we did the simplest thing to make the test compile
- now, we are going to do the simplest thing to make the test pass
- Is this process too slow?
- Yes, as you get familiar with the TDD life cycle you will gain confidence and make bigger steps
- No, taking small simple steps avoids mistakes; beginning programmers try to code too much before invoking the compiler; they then spend the rest of their time debugging!
Refactoring
- To remove the duplication of the test data and the hard-wired code of the times method, we think the following
- “We are trying to get a 10 at the end of our test case and we’ve been given a 5 in the constructor and a 2 was passed as a parameter to the times method”
- So, lets connect the dots…
Dollar Class, v. 0.2
public class Dollar {
public Dollar(int amount) {
this.amount = amount;
}
public void times(int multiplier) {
amount = amount * multiplier;
}
public int amount;
}
Now our test compiles and passes, and we didn't have to “cheat”!
One loop complete!
- Before writing our next test case, we update our testing list
- 5 USD + 10 CHF = 10 USD
- $5 * 2 = $10
- make “amount” private
- Dollar side-effects?
- Money rounding?
Dollar Side Effects
- Lets address the “Dollar Side Effects?” item on our testing list next
- When we called the times operation our variable “five” was pointing at an object whose amount equaled “ten”; not good
- the times operation had a side effect which was to change the value of a previously created “value object”
- As much as you might like to, you can’t change a 5 dollar bill into a 500 dollar bill
- the 5 dollar bill remains the same throughout multiple financial transactions
Next Test Case
public void testMultiplication() {
Dollar five = new Dollar(5);
Dollar product = five.times(2);
assertEquals(10, product.amount);
product = five.times(3);
assertEquals(15, product.amount);
assertEquals(5, five.amount);
}
- Note: the last assert is redundant
- It is implicitly shown to be true by the second assert
- Sometimes its good to make things explicit!
Discussion of the Example
- There is still a long way to go
- only scratched the surface
- But
- we saw the life cycle performed three times
- we saw the advantage of writing tests first
- we saw the advantage of keeping things simple
- we saw the advantage of keeping a testing list to keep track of our progress
- As we write new code, we will know if we are breaking things because our old test cases will fail if we do
- If the old tests stay green, we can proceed with confidence
Principles of TDD (I)
- Testing List
- Keep a record of where you want to go
- Kent Beck keeps two lists
- one for his current coding session and one for “later”
- You won’t necessarily finish everything in one go!
- Test First
- Write tests before code, because you probably won’t do it after
- Writing test cases gets you thinking about the design of your implementation
- does this code structure make sense?
- what should the signature of this method be?
Principles of TDD (II)
- Assert First
- How do you write a test case?
- By writing its assertions first!
- Suppose you are writing a client/server system and you want to test an interaction between the server and the client
- Suppose that for each transaction, some string has to have been read from the server and that the socket used to talk to the server should be closed after the transaction
- Lets write the test case
public void testCompleteTransaction {
…
assertTrue(reader.isClosed());
assertEquals(“abc”, reply.contents());
}
- Now write the code that makes these asserts possible
public void testCompleteTransaction {
Server writer = Server(defaultPort(), “abc”)
Socket reader = Socket(“localhost”, defaultPort());
Buffer reply = reader.contents();
assertTrue(reader.isClosed());
assertEquals(“abc”, reply.contents());
}
- Now you have a test case that can drive development
- if you don’t like the interface above for server and socket; then write a different test case
- or refactor the test case, after you get the above test to pass
Summary
- Test-Driven Design is a “mini” software development life cycle that helps to organize coding sessions and make them more productive
- Write a failing test case
- Make the simplest change to make it pass
- Refactor to remove duplication
- Repeat!
Reflections
- Test-Driven Design builds on the practices of Agile Design Methods
- If you decide to adopt it, not only do you “write code only to make failing tests pass” but you also get
- an easy way to integrate refactoring into your daily coding practices
- an easy way to introduce “integration testing/building your system every day” into your work environment
- because you need to run all your tests to make sure that your new code didn’t break anything;
- this has the side effect of making refactoring safe
- courage to try new things, such as unfamiliar design pattern, because now you have a safety net
Experience
- TDD can indeed be useful out in the “real world”
- I taught a version of this lecture back in 2003 and one student really took it to heart. After she graduated I would get messages like the following from her:
Dr. Anderson,
I hope you don't mind hearing from former students :) Remember me
from Object Oriented Analysis and Design last spring? I'm now happily
graduated and working in the so-called 'Real World' (yikes).
I just wanted to give you another testimony on the real-life use of
test driven development. My co-workers are stunned that I am actually
using something at work that I learned at school (well, not really,
but they like to tease). For a new software parsing tool I'm
developing, I decided to use TDD to develop it and it is making my
life so easy right now to test new changes.
Anyways, I just thought of you and your class when I decided to use
this and I wanted to let you know.
I hope that you are doing well. Best of luck on this new semester.
Cheers,
Edith
Testing Frameworks
- Lets look at examples of
- JUnit
- PyUnit
- doctest (also for python)