These lessons will teach you the basics of Test Driven Development (TDD) in Java, using JUnit, Mockito, and IntelliJ.
We’re assuming that we don’t need to convince you why you want to do TDD and we’ll only touch lightly on the principles of TDD. Instead we’ll be focusing on the what and how.
TDD is the practice of writing a small amount of code (a unit test) that describes the new behavior you wish to add to your program before you implement the behavior itself.
You can think of unit tests as tiny programs we write to verify that the methods in our classes do what we expect them to do.
The following sequence is based on the book Test-Driven Development by Example.
The basics steps for this process look like this:
-
Write a small failing unit test
-
Make this new test pass in the simplest way possible
-
Clean up any messes we created
In test-driven development, each new feature begins with writing a test. This test must inevitably fail because it is written before the feature has been implemented. (If it does not fail, then either the proposed "new" feature already exists or the test is defective.) To write a test, the developer must clearly understand the feature's specification and requirements. The developer can accomplish this through use cases and user stories to cover the requirements and exception conditions, and can write the test in whatever testing framework is appropriate to the software environment. This could also be a modification of an existing test. This is a differentiating feature of test-driven development versus writing unit tests after the code is written: it makes the developer focus on the requirements before writing the code, a subtle but important difference.
This validates that the test harness is working correctly and that the new test does not mistakenly pass without requiring any new code. This step also tests the test itself; it rules out the possibility that the new test always passes, and therefore is worthless. The new test should also fail for the expected reason. This increases confidence (though does not guarantee) that it is testing the right thing, and passes only in intended cases.
The next step is to write code that causes the test to pass. The new code written at this stage is not perfect, and may, for example, pass the test in an inelegant way. That is acceptable because later steps improve and hone it.
At this point, the only purpose of the written code is to pass the test; no further (and therefore untested) functionality should be predicted and 'allowed for' at any stage. This prevents unnecessary and unspecified code from being written, helping avoid YAGNI functionality.
If all test cases now pass, the programmer can be confident that the code meets all the tested requirements. This is a good point from which to begin the final step of the cycle.
Now the code should be cleaned up. Move code to where it logically belongs. Remove duplication. Make sure variable and method names represent their current use. Clarify constructs that might be misinterpreted. Use Kent Beck's four rules of simple design to guide you, as well as anything else you know about writing clean code. By re-running test cases, you can be confident that refactoring is not damaging any existing functionality.
The concept of removing duplication is an important aspect of any software design. In this case it also applies to removing duplication between test code and production code—for example magic numbers or strings repeated in both to make the test pass in the "Write some code" step.
Starting with another new test, repeat the cycle to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in preference to excessive debugging. Continuous integration helps by providing revertible checkpoints. When using external libraries do not make increments so small that they merely testing the library itself, unless there is some reason to believe that the library is buggy or not sufficiently feature-complete to serve all the needs of the main program being written.
As we mentioned above, unit tests are small programs that we use to verify the correctness of our "production" code. The word unit refers to a subdivision of the overall program. While others might consider unit to mean a class or package, we will only be unit testing at the method level.
Before you start writing a unit test you should know what behavior you want to verify. For instance, you might have a method that returns the plural version of a word. In English, the way we pluralize most words is by adding the letter ‘s’ to the end of the word. That sounds like a great first test case.
Once you know what behavior you want to verify you can name your test. A great format for test names is,
should<expected behavior>When<situation that behavior depends on>
. In our pluralizing example, the expected
behavior is ‘add an s’ and the situation is ‘normal word’. That means that we could name out test
shouldAddSWhenWordIsNormal
. Since it’s not necessarily clear what it means for a word to be ‘normal’, we could also
name the test shouldAddS
or shouldAddSToWord
.
Once you know the behavior you want to verify and the method where you expect add that behavior, you can start writing your test. We’ll show you how to do this in JUnit.
JUnit is a popular Java unit testing framework. We’re going to use JUnit to create our TDD unit tests.
JUnit example:
public class PluralizerTests {
...
@Test
public void shouldAddSWhenWordIsNormal() {
// Arrange our objects
Pluralizer pluralizer = new Pluralizer();
// Action we are testing
String result = pluralizer.pluralize("Cat");
// Assert that the action caused the expected result
assertThat(result, is("Cats"));
}
...
}
We call the class that we are testing the class under test. In the example above, the class under test is
Pluralizer
. All of the unit tests for the class under test will live inside a test class named:
<class under test>Tests.java
.
There are three sections to every unit test. One set of names for these sections is: Arrange, Action, Assert. Another is: Given, When, Then.
This is where we set the stage for our scenario. That means that we create all of the objects we need for the test in this section. While arranging happens at the top of our test, we often make changes here after working on the other two sections.
The Action/When section is where we call the method that we are testing (the Action). This should usually be a single method call.
We verify that the method under test caused the right thing to happen in Assert/Then section of our tests. If you feel like you need more than one assert you should probably split your test.
If you have a good idea of the behavior you want to test then you should just do this:
-
Create the test method with a name that describe what you are testing
-
Write the test in whatever way makes sense to you
If you don’t have a good sense of what your test to look like when you start writing it, try using this process:
-
Write an empty test with an meaningless name
-
Create an instance of the class you want to test
-
Call the method you want to test
-
Setup the case that will cause the result
-
Assert some result you expect
-
Name your test
Here’s an example of Test Driving a StringJoiner
class whose job is to Join strings.
Join - Joining a list of strings means creating a single new string by concatenating the string in list together with a delimiter between them. For instance, joining the strings {"a", "b”, "c”} on the delimiter ",” would result in the string "a,b,c”. Note that there is not a leading or trailing comma.
- Create all of the test scaffolding and get it to compile (name your test something ugly).
public class StringJoinerTests {
@Test
public void shouldFooWhenBar() {
}
}
2 & 3) Next, create an instance of the class you are testing and call the method. You should type out the name of the
class and method even if they don’t exist yet. In the example below, assume that the class StringJoiner
doesn’t
exist yet.
public class StringJoinerTests {
@Test
public void shouldFooWhenBar() {
String result = new StringJoiner().join();
}
}
Right now the class StringJoiner
doesn't exist. This means we need to create it. You can create it manually or
use your IDE to create it for you.
IntelliJ will highlight
StringJoiner
in red. We can click on the class name, press Alt-Enter and choose the optionCreate Class ‘StringJoiner’
. This will automatically create the class for you.
After the class is created our test will look like this...
public class StringJoinerTests {
@Test
public void shouldFooWhenBar() {
String result = new StringJoiner().join();
}
}
Now the join
method doesn't exist. Create it yourself of use your IDE to create it for you.
In IntelliJ
join
will be red because it’s not implemented. Click on the method name, hit Alt-Enter, and chooseCreate Method ’join’
.
Now we’re calling the method we want to test. Here are some useful questions we can ask:
-
What is the smallest piece of new behavior that we can add?
-
What change would it cause?
These questions should lead us to add an assert that verifies that we got the correct result from joining some strings.
Right now the join
method returns null
. Let’s add the behavior that causes join
to return the empty string when
the list that is passed in is empty.
Wait a minute, we aren’t passing a list of strings into the join
method yet. We also suddenly have enough
information to name our test. And we want to add an assert too. When you have more than one thing that could be your
next change, write down the options and do them one at at time. Here’s our current ToDo list:
-
Pass list of strings to join
-
Add assert to test
-
Rename this test to ???
- Let’s pass an empty list into
join
first.
public class StringJoinerTests {
@Test
public void shouldFooWhenBar() {
List<String> strings = new ArrayList<String>();
String result = new StringJoiner().join(strings);
}
}
- Now we can add our assert that verifies that we got an empty string back.
public class StringJoinerTests {
@Test
public void shouldFooWhenBar() {
List<String> strings = new ArrayList<String>();
String result = new StringJoiner().join(strings);
assertThat(result, is(""));
}
}
- Now that we really understand what we are testing we can rename our test. Normally you will name your test before you start writing it. You should only name it later when you have trouble understanding what you want to test. Let’s rename the test based on what we know about the purpose of our test.
public class StringJoinerTests {
@Test
public void shouldJoinIntoAnEmptyStringWhenListIsEmpty() {
List<String> strings = new ArrayList<String>();
String result = new StringJoiner().join(strings);
assertThat(result, is(""));
}
}
This is a complete unit test. Let’s split it up to clarify what the three sections of the test are.
public class StringJoinerTests {
@Test
public void shouldJoinIntoAnEmptyStringWhenListIsEmpty() {
// Arrange
StringJoiner joiner = new StringJoiner();
List<String> strings = new ArrayList<String>();
// Action
String result = joiner.join(strings);
// Assert
assertThat(result, is(""));
}
}
We can run the test and watch it fail by clicking anywhere in the test file and hitting Ctrl-Shift-F10.
Now we want to make it pass by writing the simplest code possible. This is how we can make the test pass:
public class StringJoiner {
public String join(List<String> strings) {
return "";
}
}
This is our first passing test! If there was anything for us to refactor we would do so now. Now let’s write more tests. We’ll move a little faster now.
@Test
public void shouldJoinIntoTheStringWhenListIsOneString(){
List<String> strings = new ArrayList<String>();
String aString = "A String";
strings.add(aString);
StringJoiner joiner = new StringJoiner();
String result = joiner.join(strings);
assertThat(result, is(aString));
}
This is a much more interesting test than the first one. There’s a lot going on in the Arrange section, although most of it is just adding a string to the list. Other than the change to how we arrange the objects this test is mostly the same as the first one. The test fails as expect and this is a simple way to make it pass.
public class StringJoiner {
public String join(List<String> strings) {
if (strings.size() > 0){
return strings.get(0);
}
return "";
}
}
Now we want to run both of our tests. A simple way to do this is to click anywhere in the test file that is not inside
of a method and hit Ctrl-Shift-F10. Now both tests pass and it’s time to think about refactoring. There’s nothing
obvious to refactor in StringJoiner
, but there’s a lot of duplication in our test class. After removing comments
and blank lines, it looks like this:
public class StringJoinerTests {
@Test
public void shouldJoinIntoAnEmptyStringWhenListIsEmpty(){
List<String> strings = new ArrayList<String>();
StringJoiner joiner = new StringJoiner();
String result = joiner.join(strings);
assertThat(result, is(""));
}
@Test
public void shouldJoinIntoTheStringWhenListIsOneString(){
List<String> strings = new ArrayList<String>();
String aString = "A String";
strings.add(aString);
StringJoiner joiner = new StringJoiner();
String result = joiner.join(strings);
assertThat(result, is(aString));
}
}
The new ArrayList<String>()
and new StringJoiner()
lines are exactly the same in both tests. Let’s fix this while our tests are passing so we can have
confidence that we didn’t break anything. We can move change these local variables into instance variables which we
initialize in setup method like this:
public class StringJoinerTests {
private List<String> strings;
private StringJoiner joiner;
@Before
public void setUp() throws Exception {
strings = new ArrayList<String>();
joiner = new StringJoiner();
}
@Test
public void shouldJoinIntoAnEmptyStringWhenListIsEmpty(){
assertThat(joiner.join(strings), is(""));
}
@Test
public void shouldJoinIntoTheStringWhenListIsOneString(){
String aString = "A String";
strings.add(aString);
assertThat(joiner.join(strings), is(aString));
}
}
Note that we removed the result
variable to improve readability.
There’s also a new method call setUp
which has the @Before
annotation. Any method that is marked with
@Before
will be executed before each test in that same class. This allows us to reset the strings and joiner
instance variables so that they don’t allow the actions of one test to affect another.
So far we have taken such small steps that out StringJoiner
class doesn’t do much. This is normal for TDD, we’re
implementing the behavior we want in very small slices but they will quickly add up to everything we need. Here’s our
next test.
@Test
public void shouldContainBothStringsWhenListIsTwoStrings(){
strings.add("A");
strings.add("B");
assertThat(joiner.join(strings),
both(containsString("A")).
and(containsString("B")));
}
The assert in this test is more complex than in previous tests. ContainsString
verifies that the result of join
contains a certain string (in this case "A" or "B”). The both/and
construct means that both containsString
verifications must be true for the assert to pass.
When we run all of our tests, we’re happy to see that this new test fails and all of our old tests pass. Now we need to make the new test pass.
public class StringJoiner {
public String join(List<String> strings) {
String result = "";
for (String string : strings) {
result += string;
}
return result;
}
}
This makes all of our tests pass. Great! So far our StringJoiner
joins all of the strings together, but it doesn’t
even know what a delimiter is, much less how to put it between the strings in the list. Our new test should fix that…
@Test
public void shouldPutDelimiterBetweenStrings(){
StringJoiner joinerWithDelimiter = new StringJoiner(",");
strings.add("A");
strings.add("B");
assertThat(joinerWithDelimiter.join(strings), is("A,B"));
}
Our StringJoiner
now knows about delimiters and it’s constructor takes one as a parameter. Because our constructor
changed we have to update all of the places that we create a new StringJoiner
. Fortunately, there is only one
other new StringJoiner
in our tests because we removed duplication early on and moved creation of our
StringJoiner
into the setUp
method. Now we need to figure out the simplest way to make all of our tests pass.
public class StringJoiner {
private String delimiter;
public StringJoiner(String delimiter) {
this.delimiter = delimiter;
}
public String join(List<String> strings) {
String result = "";
if (strings.size() > 0){
List<String> allExceptFirstString =
new ArrayList<String>(strings);
result += allExceptFirstString.remove(0);
for (String string : allExceptFirstString) {
result += delimiter + string;
}
}
return result;
}
}
This sure doesn’t look simple, but it was easy to implement and is a small incremental change to our previous code. We know that our code does the right thing because all of our tests pass now and our most recent test didn’t pass before we wrote this code.
This is the first time that we’ve had code that we might want to refactor. It’s safe to refactor because all of our code is covered by tests and all of those tests are passing. Here’s a slightly cleaner version of the code.
public class StringJoiner {
private String delimiter;
public StringJoiner(String delimiter) {
this.delimiter = delimiter;
}
public String join(List<String> strings) {
if (!strings.isEmpty()){
String firstString = strings.get(0);
List<String> remainingStrings =
strings.subList(1, strings.size());
return firstString +
concatenateWithDelimiter(otherStrings);
}
return "";
}
private String concatenateWithDelimiter(List<String> strings) {
String result = "";
for (String string : strings) {
result += delimiter + string;
}
return result;
}
}
As a result of our disciplined practice of TDD, we have evidence that our code is correct and we were able to safely refactor it into code that is easier to read, extend, and test.
- Clone the git repo
- Go to the repository page for this tutorial
- Click the
Copy to clipboard
button next to theHTTPS clone URL
label - At the command line, go to your projects directory (you may need to create one) and type
git clone
and past the URL of the repo (this is in your clipboard from the previous step). Then hit enter. This should create a new directory named TDDIntro in your projects directory and copy a bunch of files into it. - Open TDDIntro in IntelliJ
- Open IntelliJ
- Choose
Import Project
and select the root directory of the TDDIntro project. - Make sure that
Import project from external model
andGradle
are selected and hitNext
. - If the
Use default Gradle wrapper
radio button is selected then hitFinish
and you are ready to go. - Otherwise, click the
Use local Gradle installation
radio button and browse to the gradle directory inside this project. Then hitFinish
and you are ready to go.
Open the class com.thoughtworks.factorial.FactorialTests
. You'll find five unit tests there. Your goal is to make
changes to the class Factorial
so that one more test passes than the last time you made a change. Essentially,
you're doing the Make the failing test pass step of TDD. This should help you get used to the rhythm of TDD before
you have to write your own tests. Here's the cycle you should go through once for each test.
- Run all of the tests by clicking anywhere inside the test class between the test methods and then hit Control-Shift-F10.
- Look at the assert line of the test you are trying to make pass (do them in order) and change the
compute
method so that the assert will pass. - Run all of the tests. The only new test that should pass is the one you are currently trying to make pass. If more than one new test passes, you are adding too much functionality. Revert back to the last time you made a new test pass and try again. You should also try again if one of the previously passing tests now fails.
- Now that you have one more test passing, you should commit you change so you can revert back to a good state later if you need to.
Now you're going to write your own test.
Look in the class com.thoughtworks.accountbalance.AccountTests
. You'll see three commented out empty unit tests
(one for each of the test cases listed below).
For each of the test cases:
- Implement the test for that test case. Uncomment it and add a test code inside it.
- Fix compile errors.
- Watch the test fail.
- Write now code that you expect to make the test pass.
- Watch the test pass. If any of your tests fail, you should you should repeat step #4.
- Commit your changes and go back to Step #1 for the next test case.
Given | When | Then |
---|---|---|
I have $100 in my account | I deposit $50 | I see that my account contains $150 |
I have $100 in my account | I withdraw $50 | I see that my account contains $50 |
I have $50 in my account | I withdraw $100 | I see that my account contains $50 |