Test-Driven-Development Tutorial: Writing your first unit test

Tdd tutorial: Writing your first unit test

What is Test Driven Development?

In order to explain why Test Driven Development (TDD) is so useful and why you should incorporate it into your day-to-day job of a software developer I first need to explain what it is and provide an example of how it works.

The purpose of TDD is to first write code that works and later apply the principles of clean code to improve the readability while assuring that your code still works as expected.

This process is iterative and allows the code to provide feedback between changes.

More specifically this is the rhythm which serves as the sole of TDD. Everything you learn afterward like the principles (discussed later) will help you master TDD. Practice the rhythm first and master it, then later once you’re confident and the time is right you can go onto mastering it by learning the principles of TDD.

To achieve clean code that works we must drive development with automated tests. This leans back onto the rhythm of TDD.

The rhythm of TDD embraces that we must design with the intent of communicating with others and further to eliminate duplication in our code.

This simple rule forces us to think about our design and how to achieve low coupling and high cohesion in our components.

This allows you as the software developer to imagine and design how you’d interface with your implementation based on the interaction with your API by providing commands to objects and expecting certain results from those commands given.

This link between what you are providing and what you are expecting is the assertion that your software actually works.

Following this technique of development allows you to reduce your fear of writing code that doesn’t work because you are only writing code to satisfy an automated test.

Introduction to the TDD rhythm

Remember the rhythm I’m always talking about to writing tests in a Test-Driven way? Here it is in its entirety and it is as follows:

  1. Quickly add a test.
  2. Make it compile.
  3. Run this test to check that it fails.
  4. Make it run (make it pass).
  5. Make it right (refactor to clean code).

The common approach with TDD is to imagine our API for the client’s use (what it will look like from the outside). Describing the API is like telling a story of the steps in an operation. But not all stories are beautiful, we’ll have to work our way backward to write codes that offer simplicity first to later write more complex code that becomes uglier and realistic in its design. Then, of course, cleaning it up to make it beautiful.

The blogging platform example

The source code for this example can be found on GitHub

Imagine a scenario where you’re implementing some functionality to authenticate a user who is trying to log into your software with a provided username and password. Second, specific methods (that we define) are restricted to require correct authorization and cannot be called without first logging in.

In the context of a blogging platform software, authorization is common in the following use case: A user has to be an owner of a blog post to enable modification, removal or publication of their post. No other user (except an admin or security roles who are permitted) can modify, remove or publish their blog post. Authentication and authorization patterns like these are common throughout many software systems, where we want to enforce ownership and privacy of data.

What kind of design will we need to authenticate a user and programmatically allow some actions (e.g. publishing a new post) to require our user to be first authenticated (check the identity) and authorized (has permission to) to proceed with the action?

We need to have the ability to authenticate a user based on their credentials. This is the same process of logging into the software. This includes an operation of validating the user’s username and password. The next part, of course, is to store that user in memory that’s easily queryable for use on authorization (before allowing execution on certain methods) in our software. This pattern is commonly known as a user session.

To keep what we just discussed in mind it’s a good practice to maintain a little to-do list of what we need to do and to remind ourselves of the tasks that need to be done. We will add to this list along the way as we encounter more tests that we want to add. We’ll start with the first item as that seems to be the most interesting.

  1. Authenticate the user with username and password.
  2. User Lookup.
  3. Queryable user session.
  4. Include a pre-authorization check for methods that require authentication.

Writing your first test

So what test do we need to add first to get the ball rolling? In other words, what should our first test look like? How can we model it in such a way that maintains simplicity and does not jump straight to realism? Let’s just give it a bash:

@Test
public void testUserLoginWithCorrectCredentials() throws Exception {

    // Given that this current user exists
    String username = "adrianvdh";
    String password = "hello123";
    AuthenticationService authenticationService = new AuthenticationService();

    // When authenticating
    authenticationService.login(username, password);

    // Then the current user must be valid
    Assert.assertEquals("adrianvdh", authenticationService.currentUser.username);
    Assert.assertEquals("hello123", authenticationService.currentUser.password);
}

Principle 1: Avoid realistic design

If we wrote our test with the intent of being realistic about our design we would slow down the feedback loop to minutes rather than ideally seconds.

Realistic design given in a test is less focused on our intent which introduces a distraction from our aim, which is working functionality.

Ideally, we would have a Session object that our AuthenticationService object injects a User object into. But we’ll only introduce that nicer design later in our Refactor to clean code step discussed in the TDD rhythm section.

At the moment we can ignore our temptations and get the same functionality that our test is asking for through a naive design but not the ideal one. This act of distraction is true because I initially tried describing Session object as a singleton object. Then in the proceeding step (Make it run) implementation of our Session object’s functionality required much more effort.

Avoid this like you would flews, loneliness, and debt!

If you ran this code, it wouldn’t compile. The next step it to get it to compile.

There are quite a few compilation errors to fix that we have introduced in our test. We have 6 compilation errors by the way.

  • No class AuthenticationService.
  • No method login(String, String) for our AuthenticationService class.
  • No instance field currentUser for our AuthenticationService class.
  • No User class with fields username and password.

Rats! That looks like a lot of work for such a little test; in that case, the key is to have a positive attitude towards what we do. Let’s get a move on!

We’ll create our AuthenticationService class first, like this:

public class AuthenticationService {
}

Then implement the login(String, String) method:

public class UserService {
    public void login(String username, String password) {

    }
}

We also need our currentUser instance field for our AuthenticationService class:

public class AuthenticationService {
    public User currentUser;

    public void login(String username, String password) {

    }
}

And finally the User class itself:

public class User {
    String username;
    String password;
}

Now our test runs but fails! But this is progress thus far.

We expected two results in our test, the first expected value of “adrianvdh” got an actual null as a result.

The next step to solve this problem is to use the Fake It principle.

Principle 2: Fake it (until you make it)

The quickest solution to get the test to pass is to use the Fake It principle/approach. The idea is that you simply return constant values to get your test to pass, and then gradually swap out the constants for expressions that return the same value as the constants did.

When the login(String, String) method is invoked we’ll instantiate our currentUser field with a new User instance which has values hard-coded already.

public class AuthenticationService {
    public User currentUser;

    public void login(String username, String password) {
        currentUser = new User();
    }
}

Then assign hard-coded values onto our User class’ properties:

public class User {
    String username = "adrianvdh";
    String password = "hello123";
}

Behold! Our test passes! 😀

What’s the point of our first test? It’s there but doesn’t add any value as we are just testing hard-coded/fake values. So what gives?

The point of the initial test with hard-coded values is to act as a test net just in case we lose our grip on the wall of code and fall down which we’ll have something to land onto.

Often the first approach to getting your test bar to go green is to fake it. Our net will support us for any future changes that we make to our code as we (to quote Kent Beck) “gradually transform our constants into expressions using variables”.

But we are not done yet! We are missing the last step from the rhythm of TDD, refactor to remove duplication (make it right). In our case, we have duplication in our User class and in our test data.

There isn’t a single step to remove duplication. We are going to be brave and stretch our little leg just a bit further to be courageous:

public class AuthenticationService {
    public User currentUser;

    public void login(String username, String password) {
        User foundUser = new User();
        foundUser.username = username;
        foundUser.password = password;

        if(foundUser.username.equals(username) && foundUser.password.equals(password)) {
            currentUser = foundUser;
        }
    }
}

And in another step we need to remove that duplication from the User class too:

public class User {
    String username;
    String password;
}

I don’t like the current state of our code. The test will always pass and never fail, but there is a good reason too.

We need to mock out our queried (found) user with credentials matching the arguments passed in through our method. This found user will be replaced in the next step with a lookup strategy, but that isn’t what matters at the moment. Our authentication logic is what really matters. It’s what asks the question of, do the credentials that are passed in actually belong to anyone in the system? If so create a session for them.

We can finally knock off our first item on our todo list and highlight the next item:

  1. Authenticate user with username and password.
  2. User lookup.
  3. Queryable user session.
  4. Include a pre-authorization check for methods that require authentication.

User lookup

The last section went by really slowly. Do we have to always take such small steps when practicing TDD? No of course not! The practice is about being able to take small incremental steps. That doesn’t necessarily mean that you always should, but use it as a tool for when things are a bit hairy and you need a magnifying glass and a pair of tweezers to do your job 😉

Our AuthenticationService class has a fake implementation thus far. There is no way to lookup a user based on their username and at the moment we’re kind of reliant on a mock user. The better solution in my mind is quite simple:

public class AuthenticationService {
    public User currentUser;

    List<User> users = new ArrayList<>();
    {
        User user = new User();
        user.username = "adrianvdh";
        user.password = "hello123";
        users.add(user);
    }

    public void login(String username, String password) {
        User foundUser = users.get(0);

        if(foundUser.username.equals(username) && foundUser.password.equals(password)) {
            currentUser = foundUser;
        }
    }
}

That should suffice for the previous implementation and our test still passes! We just needed to fake it for the sake of getting the green on our test. But we still need to add the ability to find a user by their username.

AuthenticationService class: (I have omitted the class structure for brevity)

public void login(String username, String password) {
    User foundUser = null;
    for (User userInCollection : users) {
        if (userInCollection.username.equals(username)) {
            foundUser = userInCollection;
            break;
        }
    }

    ...
}

There. Now we are querying the user based by his username.

The test still passes and all is well (for the most part). But what if the user could not be found? Then what should we do? Will we still interact with our AuthenticationService object the same way? Our guess about designing the interface of AuthenticationService class is no more perfect than our guess about the behavior of our implementation.

Well If we change the way we describe our interaction with our interface (provide invalid user credentials) then by default we have to change the way our implementation will handle that interaction by throwing an exception.

Currently, now the behavior of our login() method will do nothing if the credentials don’t match anybody. We need to alert the user that something went wrong.

Adrian van den Houten

I'm Adrian van Houten, founder of ScholarCoder and a passionate software developer for full-stack web development. Read more about me here.