The Librarian: Introduction to Test-Driven Development
This will be a series of articles revolving around unit testing where I will work through examples and exploring various aspects of the craft. This is the first installment.
The code associated with this article can be found on GitHub. Future and past installments can be found in The Librarian Archive.
I will try to implement a few requirements for a Library
module with books and memberships, extending whatever code we have in a test-driven style (“TDD”) as we go along. I share a few thoughts about the process, show some refactorings and give a few hints for using the IDE.
The level of this article is for junior developers who want to expand their testing horizon.
There’s plenty of information out there which describes what TDD or Test-Driven Development is, the red-green-refactor cycle etc so I won’t delve into too much introductory detail here. See the references at the end for more background-information.
Instead, just get started!
We’re starting out with an Gradle project with the following build.gradle
file:
apply plugin: 'java' repositories { jcenter() } dependencies { testCompile 'junit:junit:4.12' }
This states we’re in a clean Java project, can ceate our tests in src/test/java
and sources in src/main/java
and we have a dependency on JUnit – our testing framework of choice. I could have chosen TestNG or Spock, but that’s a topic of another post.
First story in micro-steps
We’re driving the code by tests and we’re driving the tests by requirements. That’s the cycle we’re repeating.
If we were in an agile project requirements might come in the form of a user story like:
As a librarian,
I want book-loving persons to become members of the library
So that I can lend out books to them in the future
We can identify a few concepts here: a library, persons, (becoming a) member, (lending) books. Let’s concentrate on, what seems, our central concept: the Library.
So, you might be tempted to dive in and create e.g. a Library
class and code away, but we shall do no such thing!
Make a failing test
We’ll start with a failing test.
package example; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void test() { fail("Not yet implemented"); } }
A public method called test()
, annotated with JUnit’s @Test
annotation. You can see a staticly imported method fail()
here giving us a solid, failure when we run the LibraryTest
class — either with our IDE or through Gradle.
java.lang.AssertionError: Not yet implemented at org.junit.Assert.fail(Assert.java:88) at example.LibraryTest.test(LibraryTest.java:11) <snip>
Intention-Revealing Name
We’re going to rename the method to a more suitable test name which reveals the intention of our first test, which is testing that members should be able to get registered.
Let’call it something like shouldRegisterMembers()
.
package example; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void shouldRegisterMembers() { fail("Not yet implemented"); } }
(Yes, you can run it, but it will still fail)
Ok, but now what? I don’t have any code to invoke
Well yeah. We’re going to change that.
By a series of well-executed refactorings, hopefully performed by your IDE, we will create just-enough code to pass the test. By doing so, we will create our production code; the code for a Library
class which does not exist yet. The rule for now is: if there is no test requiring a piece of production code, we will not write it.
We need a Library
class to register members for. So I’m writing down the only code I need at this moment.
package example; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); } }
If you are in an IDE it will signal something like “Library can not be resolved to a type”. Yes, that’s your friendly compiler being a helpful fellow. You need to fix this by creating the class, or have the IDE create it for you.
In e.g. Eclipse you can choose a Quick Fix, called “Create class Library”.
Use the IDE, Luke!
Performing code modifications, such as creating missing classes or methods, are no-brainers for modern IDEs and I highly recommended you always use them for these kind of tasks.
Our newly, created Library
looks like
package example; public class Library { }
which is all we need to compile the test class and run the test. And pass.
A test which just instantiates a new Library
— which doesn’t do anything — isn’t adding value yet. It’s needed because we need to create our logic from the user story: “registering members”.
What better way to express this a method call named: registerMember
? We don’t know much about a member yet, but for now I am giving him or her a name – a simple String
to be passed along to identify a member.
I need to talk to my librarian later on to clarify all the properties we expect a member to have.
So, we’re registering a member, providing an example value of “Ted”. The code will now look like:
public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); // when library.registerMember("Ted"); } }
The LibraryTest
again does not compile anymore. The compiler will complain:
The method registerMember(String) is undefined for the type Library
Create the missing method
Use the IDE to create this method in the Library
class. Not much to look at eh? Now, we’re going to do something new: add some Javadoc to it, describing what it does.
Still not much, but the LibraryTest
compiles again.
package example; public class Library { /** * Registers a new member using provided name. * * @param name * The name of the member */ public void registerMember(String name) { } }
The test also passes.
Since we’re still not convinced we’ve implemented our logic (since we haven’t implemented anything yet really) let us design things in such a way that, if registering a member went successful, the library will give us a new, full-fledged Member
back.
We adjust the test like this: have the registerMember
method return the successfully created Member
, so we possibly can check whether the member’s name equals the one we provided.
public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); // when Member newMember = library.registerMember("Ted"); // then check for member's name to be same } }
That’s right, the registerMember
method at the moment returns void
: if we want a Member
we’ll have to change the method’s return type for the test to compile again. In order to do that, we’ll first have to create a Member
class because that doesn’t exist yet either.
public class Member { }
public class Library { /** * Registers a new member using provided name. * * @param name * The name of the member * @return */ public Member registerMember(String name) { } }
Can we actually keep changing and creating stuff like this – driven by the tests?
Yes.
Almost there. If we want to check the name of the member we can use Hamcrest matchers equalTo
and is
with a yet-to-be-made getName()
— to compare the name “Ted” from the input with the one in Member
.
package example; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); // when Member newMember = library.registerMember("Ted"); // then assertThat(newMember.getName(), is(equalTo("Ted"))); } }
Now what do we need to pass the test?
- To compile, the
Member
class will need a getter, calledgetName()
. By convention, but also needed by the code we will put inregisterMember
, makename
a final property (immutable, no setter) which we’ll initialize by a constructor. Execute a double-whammy and make this so:
public class Member { private final String name; public Member(String name) { this.name = name; } public String getName() { return name; } }
- Create a
Member
and return it.
public class Library { /** * Registers a new member using provided name. * * @param name * The name of the member * @return registered member */ public Member registerMember(String name) { return new Member(name); } }
Phew! The test passes.
It looks like we have fulfilled our requirement as per user story, which was
As a librarian,
I want book-loving persons to become members of the library
So that I can lend out books to them in the future
Is the requirement fulfilled?
As a small side-note: I’m looking at the story and I’m seeing “members”, plural. As my test is testing the library for just one member, I’m taking the liberty to beef things up and test for more members.
The minimum amount of member registrations tested — I need to get enough confident about the library supporting “members” (plural) — is two, right?
Modified the test a bit to include Ted and Bob now.
package example; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); // when Member newMember1 = library.registerMember("Ted"); Member newMember2 = library.registerMember("Bob"); // then assertThat(newMember1.getName(), is(equalTo("Ted"))); assertThat(newMember2.getName(), is(equalTo("Bob"))); } }
Test passes.
2nd story – somewhat faster
Let’s implement a 2nd user story. It goes like this:
As an accountant,
I want a person being able to register for a membership just once
So that I don’t end up having multiple memberships for the same person
When you look at it, the first user story implemented in code isn’t really a big deal. We haven’t gold-plated anything, we don’t have anything there we haven’t tested. You know the drill of make-a-failing-test, fix and repeat.
Bit of design
Let’s create a new test called shouldNotRegisterAgainWhenAlreadyMember
– clever huh?
package example; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; import org.junit.Test; public class LibraryTest { @Test public void shouldRegisterMembers() { ... } @Test public void shouldNotRegisterAgainWhenAlreadyMember() { // given Library library = new Library(); library.registerMember("Ted"); // when we register with same name library.registerMember("Ted"); // then we should see it fail somehow } }
This is where a bit of design comes in. How can we have the code tell us a member by the same name is already registered? If this normally would not be possible but we still have to deal with these kind of situations, use of Java’s exception mechanism could be a candidate.
We’ll create an AlreadyMemberException
– which the method registerMember()
can throw to indicate this exceptional event.
public class LibraryTest { @Test public void shouldRegisterMembers() { ... } @Test public void shouldNotRegisterAgainWhenAlreadyMember() { // given Library library = new Library(); library.registerMember("Ted"); // when we register with same name try { library.registerMember("Ted"); fail("should not have registered Ted twice"); } catch (AlreadyMemberException e) { // success! } } }
What happens?
- As part of the setup of the test (under
//given
), the library starts out with an existing member with the name “Ted” - When the library is told (“don’t ask”) to register another member for the same name “Ted”, we expect an
AlreadyMemberException
to be thrown: theLibrary
shouldn’t allow multiple members with the same name - If
registerMember("Ted")
does not throw the exception, we’ll fail the test onfail()
- There might be a more elegant way of expecting a certain exception to be thrown, but we won’t want to get ahead of ourselves
Right now the test fails – NO exception is thrown since we haven’t updated the Library
yet.
Let’s do that now.
The code should somehow now track members internally, else it wouldn’t be able to remember members registered between invocations.
The simplest solution (KISS) is using a data structure from the Collections Framework for that. Any Collection
, such as ArrayList
, has methods such as contains
(to check for presence) and add
– to fulfill all our needs:
- check for a member
- add a member
Our solution would look like this:
package example; import java.util.ArrayList; import java.util.Collection; public class Library { private final Collection<Member> members = new ArrayList<>(); /** * Registers a new member using provided name. * * @param name * The name of the member * @return registered member */ public Member registerMember(String name) { Member newMember = new Member(name); if (members.contains(newMember)) { throw new AlreadyMemberException(); } members.add(newMember); return newMember; } }
Run the our test method and… see it still fail. WAT?
Know your framework
Nobody is throwing an exception, but the code is simple enough to expect that contains
and add
ing should Just Work. Is it a flaw in the test itself?
No, one has to know to when putting objects like Member
in a Collection
and we want the Collection
check for equality (and not identity) we need to implement equals()
and hashCode()
in Member
.
You could generate these methods with any decent IDE (or invoke helper methods from helper frameworks for doing this) but code below (generated by Eclipse) will suffice:
package example; public class Member { private final String name; public Member(String name) { this.name = name; } public String getName() { return name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Member other = (Member) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } }
Test passes!
Refactor
Now that we have some test coverage, we can do something we left out up until now: refactor!
The rules are:
- Make it work
- Make it better (faster etc)
- Make it readable (DRY, maintainable etc)
We made it work. By the process of refactoring – which often is described as “series of small behavior preserving transformations” we can tackle the remaining two bullets: make it “better” and readable if this is possible. And this is always possible: the pitfalls I’ve encountered over the years is that one can refactor til infinity – knowing when to stop is often tricky.
These are just examples, but we could…
Simplify the implementation
We can probably make our existence-check simpler and get rid of contains
and use the return value of add
directly — if we convert to a Collection-type which prevents duplicates by itself.
We can probably replace ArrayList
by HashSet
and add
will return false
if the collection of members won’t allow adding a member it already has. This would look like:
package example; import java.util.Collection; import java.util.HashSet; public class Library { private final Collection<Member> members = new HashSet<>(); /** * Registers a new member using provided name. [...] */ public Member registerMember(String name) { Member newMember = new Member(name); if (!members.add(newMember)) { throw new AlreadyMemberException(); } return newMember; } }
Simplify the tests
Ha, you thought only the implementation would need work? No, also the tests are each iteration of test-fix-refactor eligible for polishing up.
E.g. if we look at the the part which is the same in each testmethod in LibraryTest
it’s the instantiation of a new Library()
.
public class LibraryTest { @Test public void shouldRegisterMembers() { // given Library library = new Library(); ... } @Test public void shouldNotRegisterAgainWhenAlreadyMember() { // given Library library = new Library(); ... } }
We can apply a refactoring called Convert Local Variable to Field and initialize our field before each test, using JUnit’s @Before
annotation.
public class LibraryTest { private Library library; @Before public void setUp() { library = new Library(); } @Test public void shouldRegisterMembers() { // when Member newMember1 = library.registerMember("Ted"); Member newMember2 = library.registerMember("Bob"); ... } @Test public void shouldNotRegisterAgainWhenAlreadyMember() { // given library.registerMember("Ted"); ... } }
Tests pass!
(You we’re probably wondering when I would get around to it :-))
And last, but not least – there is a way to simplify expecting a certain exception with JUnit: the ExpectedException
rule — which really gets rid of the clutter of our last test.
package example; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; public class LibraryTest { @Rule public ExpectedException thrown = ExpectedException.none(); private Library library; @Before public void setUp() { library = new Library(); } @Test public void shouldRegisterMembers() { ... } @Test public void shouldNotRegisterAgainWhenAlreadyMember() { // given library.registerMember("Ted"); // fail when we register with same name thrown.expect(AlreadyMemberException.class); library.registerMember("Ted"); } }
Tests pass!
In conclusion
I agree with some conclusions made by James Shore in an article (“How Does TDD Affect Design”) many, many years ago: TDD can lead to better design, TDD can lead to worse design. The TDD perspective is just one of many in the testing space and be a welcome addition in anyone’s tool-belt. I’d like to believe we haven’t created any production code which has not been tested, but I didn’t run any code coverage tool yet to verify if I have covered all the paths – but that wasn’t the aim for now either way
I hope the article showed a glimpse of how the TDD cycle can work and how to guide the design of our classes with our tests and end up with regression test suite as a great side-effect.
References
- https://github.com/tvinke/testing-tdd-intro Code for the example in this article on GitHub
- https://en.wikipedia.org/wiki/Test-driven_development Test-driven development Nice introduction into TDD on Wikipedia
- http://www.jamesshore.com/Blog/How-Does-TDD-Affect-Design.html James Shore on TDD and how it affects design
- http://www.jamesshore.com/Blog/Lets-Play/ Let’s Play: Test-Driven Development Screencast series featuring Java, test-driven development, and evolutionary design, by James Shore. It chronicles the development of a real software project, warts and all.
- http://martinfowler.com/articles/is-tdd-dead/ Well, is it? I had to include ofcourse this article – but think for yourself!
- http://refactoring.com/catalog/ Catalog of Refactorings Great overview by Martin Fowler about refactorings described in his book
- http://junit.org/junit4/ JUnit 4 The homepage of the used Testing Framework
- https://github.com/junit-team/junit4/wiki/Exception-testing Exception testing in JUnit. Not just the
ExpectedException
rule, but also the “expected” parameter
Reference: | The Librarian: Introduction to Test-Driven Development from our JCG partner Ted Vinke at the Ted Vinke’s Blog blog. |