Java and TDD

Hello again! In the previous blog post I explained in general, without close reference to Java, but in this part we start a TDD practice. Our goal is to go through all phases of TDD: from requirement analysis to refactoring of tested code. All this we will do on example with Java, JUnit and ‘fake’ requirements.

Requirements analysis

Let’s pretend that we need to create a new feature in a fictional application. The feature is described by the following user story:

As user I want to be able to create an Account. The Account should contain id, status (active / non-active), zone and balance. The balance property can not be negative.
By default Account should be active, in zone #1 and with 0.00 balance.

That’s how looks like an usual user story in an abstract development team. On practice the feature needs to be separated between front-end developers and back-end developers. Also we suppose that in the team already exists some code convention etc.

So after the feature was assigned to me as a to backend developer, I need to clarify all questions which are not clear for me. For example what is a purpose of the zone property?

Answer:
Zones are used in transactions of the application. Depending on zones we charge different fees from Accounts. For now we planning just 3 zones.

Ok. Now everything is clear and we can start TDD.

Java TDD: first tests

Here are dependencies which we need to have in the project:

<dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.javamoney</groupId>
            <artifactId>moneta</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>1.3</version>
        </dependency>
    </dependencies>

I use Maven, but you can use Gradle as well or any other dependency management tool. Finally I need to perform a first real development step: creation of empty Account class and appropriate test class for it. That’s project’s structure in Intellij IDEA:

TDD-project-structure

Pay attention to Account class location and to AccountTest class. They have the same packages names, but different directories. That’s some kind of convention.

Recalling the user story, I want to create following unit tests:

1. Default Account creation
2. Custom Account creation
3. Check negative balance case

Here are test methods:

package com.model;

import org.javamoney.moneta.Money;
import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class AccountTest {

    @Test
    public void defaultAccountCreationTest() {
        Account account = new Account();
        assertThat(account.getId().length(), equalTo(6));
        assertThat(account.isActive(), equalTo(true));
        assertThat(account.getZone(), equalTo(Account.Zone.ZONE_1));
        assertThat(account.getBalance(), equalTo(Money.of(0.00, "USD")));
    }

    @Test
    public void customAccountCreationTest() {
        Account account = new Account(false, Account.Zone.ZONE_3, 125.95);
        assertThat(account.getId().length(), equalTo(6));
        assertThat(account.isActive(), equalTo(false));
        assertThat(account.getZone(), equalTo(Account.Zone.ZONE_3));
        assertThat(account.getBalance(), equalTo(Money.of(125.95, "USD")));
    }

    @Test(expected = IllegalArgumentException.class)
    public void negativeBalanceTest() {
        Account account = new Account(false, Account.Zone.ZONE_3, -200);
    }

}

When the tests are completed it’s time to see what we get in the Account class. Because within unit tests development I also created dummy declaration of required methods, constructors and properties in the Account class.

Business logic implementation

When the unit tests are completed, we need to see something like this in the Account class:

package com.model;

import org.javamoney.moneta.Money;

public class Account {

    private String id;
    private boolean active;
    private Zone zone;
    private Money balance;

    public Account() {}

    public Account(boolean active, Zone zone, double balance) {

    }

    public enum Zone {
        ZONE_1, ZONE_2, ZONE_3
    }

    public String getId() {
        return id;
    }

    public boolean isActive() {
        return active;
    }

    public Zone getZone() {
        return zone;
    }

    public Money getBalance() {
        return balance;
    }

}

As you see above, the Account class isn’t so good as it should be from the functional point of view. Constructors are useless, all properties are not initialised. But a test driven development implies such situation on the stage of unit tests creation.

When we run the unit tests for the Account class we will get “red” results. So the way to do them green is to start from the defaultAccountCreationTest(). In context of this test we have to update the Account class. Changes are pretty small but after them the unit test becomes green.

package com.model;

import org.apache.commons.lang3.RandomStringUtils;
import org.javamoney.moneta.Money;

public class Account {

    private String id = RandomStringUtils.randomAlphanumeric(6);
    private boolean active = true;
    private Zone zone = Zone.ZONE_1;
    private Money balance = Money.of(0.00, "USD");

    public Account() {}
//next code is omitted, it was not affected by the first changes

You can run the updated AccountTest class. A result of the run is: one test is passed, two are failed.
Then we need to repeat this operation for each of the unit tests, until all of them become “green”.

That’s the final version of the Account class:

package com.model;

import org.apache.commons.lang3.RandomStringUtils;
import org.javamoney.moneta.Money;

public class Account {

    private String id = RandomStringUtils.randomAlphanumeric(6);
    private boolean active = true;
    private Zone zone = Zone.ZONE_1;
    private Money balance = Money.of(0.00, "USD");

    public Account() {}

    public Account(boolean active, Zone zone, double balance) {
        this.active = active;
        this.zone = zone;
        if (balance < 0)
            throw new IllegalArgumentException("The balance can not be negative");
        this.balance = Money.of(balance, "USD");
    }

    public enum Zone {
        ZONE_1, ZONE_2, ZONE_3
    }

    public String getId() {
        return id;
    }

    public boolean isActive() {
        return active;
    }

    public Zone getZone() {
        return zone;
    }

    public Money getBalance() {
        return balance;
    }

}

And here is the screenshot of the tests run:

Test-run-results

After testing Refactoring

Probably in more complex examples I would perform some refactoring after the tests became green. But in this simple case we do not need this. If you have any suggestions about increasing a code readability or style, feel free to leave your comment.

Summary

In this tutorial we examined how to develop with TDD using Java, starting from a feature analysis and finishing with “green” unit tests and refactoring. I tried to explain the TDD approach on example which isn’t too trivial but also not so complex. Any way I hope it was helpful and informative for you.

About The Author

Mathematician, programmer, wrestler, last action hero... Java / Scala architect, trainer, entrepreneur, author of this blog

  • Tomek Kaczanowski

    What bothers me here is this “status” boolean. I know this is only an example, but this is a very generic kind of field. Maybe the example would be more interesting if it was called “active” and then you could get rid off getter but have a “isActive()” method instead? Just an idea.

    P.S. And kudos for using Hamcrest matchers! 🙂

    • Good point about the status ‘field’. Will rename it in ‘active’ =)

      Thanks

Close