Unit vs. Acceptance Testing (Part 1)

by

You don’t know the power of Test Driven Development

As a recent convert to Test Driven Development (or TDD as his friends call him), I was surprised to hear that there were in fact 2 kinds of developer driven testing.  The standard one that everyone knows of is unit testing:  writing little testXXX methods that test a single publicly exposed method.  But the lesser known – but just as important – are acceptance tests:  tests which verify that  a group of classes working together properly fulfill some functionality.

So this first post will take a closer look at the first type of testing, unit tests, and will go into the value it provides to you as a developer.

Unit Testing – Testing Expectations

NOTE: You can find the code discussed in this post at this GitHub repository.

Unit tests focus on testing your assumptions as a developer on how the class being tested – the system under test or SUT – interacts with its dependencies to perform some action or calculation.  So the idea is:
  1. Pick a class or method you want to test (the SUT).  In TDD this is going to be the class or method you are going to write, but haven’t written just yet :)
  2. Mock the dependencies the SUT uses, setting the expectations (what methods on the mock should be called) and patching the return values (have the called methods return test data).
  3. Write a test verifying that an aspect of the SUT works correctly when provided with the mocked dependencies.

So, let’s look at an example.  Imagine you have the following class which has a single dependency:

public interface Dependency {
    public boolean checkSomeValue(String value);
    public String doSomethingToValue(String someValue);
}

public class ClassToTest {

    private Dependency someDependency;

    public ClassToTest(Dependency someDependency) {
        this.someDependency = someDependency;
    }

    public String methodToTest(String someValue) {
        if (someDependency.checkSomeValue(someValue)) {
            return someDependency.doSomethingToValue(someValue)
                                 .toUpperCase();
        }
        return null;
    }
}

So looking at the code, there are 4 operations that need to be tested for this method:

  1. Only if checkSomeValue returns true do you call doSomethingToValue
  2. If you don’t call doSomethingToValue, the method should return null
  3. Calls doSomethingToValue on the provided string if checkSomeValue is true
  4. Transform the return value of doSomethingToValue to all uppercase and return it

So here’s our first cut at the tests:

import static org.easymock.EasyMock.*;

import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;

public class TestClassToTest {
    Dependency mockedDependency;

    @Before
    public void setUp() {
        mockedDependency = createMock(Dependency.class);
    }

    @Test
    /**
     * methodToTest : Only does something to provided value if it passes the check value test
     */
    public void test_methodToTest_OnlyCallsDoSomethingIfValueChecks() {
        expect(mockedDependency.checkSomeValue("some value")).andReturn(false);
        replay(mockedDependency);

        ClassToTest classToTest = new ClassToTest(mockedDependency);
        classToTest.methodToTest("some value");

        verify(mockedDependency);
    }

    @Test
    /**
     * methodToTest : Returns null if the provided value does not pass the check value test
     */
    public void test_methodToTest_ReturnsNullIfValueDoesntPassTest() {
        expect(mockedDependency.checkSomeValue("some value")).andReturn(false);
        replay(mockedDependency);

        ClassToTest classToTest = new ClassToTest(mockedDependency);
        String value = classToTest.methodToTest("some value");

        assertNull("Return values wasn't null even though provided value failed check", value);

    }

    @Test
    /**
     * methodToTest : Does something to the string if it passes the check value test
     */
    public void test_methodToTest_DoesSomethingToStringIfItPassesTest() {
        expect(mockedDependency.checkSomeValue("some value")).andReturn(true);
        expect(mockedDependency.doSomethingToValue("some value")).andReturn("transformed string");
        replay(mockedDependency);

        ClassToTest classToTest = new ClassToTest(mockedDependency);
        String value = classToTest.methodToTest("some value");
        verify(mockedDependency);
    }

    @Test
    /**
     * methodToTest : Returns the upper case version of the string after something has been done to it
     */
    public void test_methodToTest_UppersTranformedStringFromDoSomething() {
        expect(mockedDependency.checkSomeValue("some value")).andReturn(true);
        expect(mockedDependency.doSomethingToValue("some value")).andReturn("transformed string");
        replay(mockedDependency);

        ClassToTest classToTest = new ClassToTest(mockedDependency);
        String value = classToTest.methodToTest("some value");
        assertEquals("Returned value wasn't uppered", "TRANSFORMED STRING", value);
    }
}

So let’s take a look at what – and how – we’re testing.

First, you’ll notice that I’m using EasyMock to create a mocked version of the Dependency class. EasyMock allows you as a tester to:

  1. Declare our expectations on how the mocked class is to be used by specifying what methods are to be called and with what arguments. This is done with the expect(...) method
  2. Declare our expectations on what the mocked class returns by specifying a return value of the mocked method. This is done by chaining the andReturn() call onto the expect() results.

After setting up our expectations on the mocked dependency, we mark it as being ready to use with the replay(...) call, inject it into the class to be tested and execute the method being tested. We then verify the results in one of 2 ways.

The first way to verify is to call the EasyMock method verify. verify asks EasyMock to validate that ONLY the expected methods were called with ONLY the expected arguments. If some other method was called on the mocked Dependency object, EasyMock would throw an exception saying an unexpected method was called.

The second is to verify the returned value from the method being tested meets our expected results. You can see this in second and forth test where I use the JUnit assertXXX methods.

Unit Testing – Documenting Assumptions

So looking at this we’re completely done and ready to move on, no? Not really.

What happens if we pass in null into methodToTest? Would that mean the checkSomeValue would return false or throw an exception?

This is where I believe unit tests provide one of it’s greatest value: unit tests force you to analyze and document your assumptions on how the class you’re depending on works.

So let’s deal with checkSomeValue method and how it deals with a null input. Let’s say that the provider of the Dependency class states that the expected behavior of checkSomeValue is to return false if the provided value is null. If so, then the check value test would fail and the method we’re testing would do what it always does when the input string fails: it returns null. So let’s write a test to assert and document this assumption!

    @Test
    /**
     * methodToTest : Returns null if the provided value is null
     */
    public void test_methodToTest_ReturnsNullIfProvidedValueIsNull() {
        expect(mockedDependency.checkSomeValue(null)).andReturn(false);
        replay(mockedDependency);

        ClassToTest classToTest = new ClassToTest(mockedDependency);
        String value = classToTest.methodToTest(null);

        assertNull("Return values wasn't null even though provided value was null", value);
    }

Some Parting Thoughts…

When I was first starting to learn Test Driven Development, my mentor who was teaching me from the murky swamps of Central Florida, told me “If it’s easy to test, it’s easy to write”. After some time working with TDD, I discovered that he wasn’t just being all Zen like, he was right. In fact, I would take what he said and add to it “If it’s easy to test, it’s easy to write, and leads to a better design”. Here’s why…

Let’s say that over time, the ClassToTest grows in scope to the point where it now has 6 objects it depends on. Imagine having to write all of those unit tests: having to write all of the setup code to create mocked versions of those dependencies, declaring the expected calls and return values in each test, having to write tests to handle what happens with all of the dependencies are called within a single method and can return different values.

It would quickly become a real pain in the neck to write all of that testing code. And I would bet that it would also be a pain in the you-know-where to code it out as well: checking return values from all of those dependencies, branching the logic flow to deal with the permutations of return values between all of those dependencies, managing all of those object life cycles.

So what would you do? You would group those dependencies by functionality and create an object to encapsulate the functionality created when those similar objects work together. Then you can refactor your class to use those higher-level objects and get rid of all of those fine-grained dependencies. Then you’re unit tests would be much easier to write, because you would have fewer dependencies to mock and fewer moving parts to worry about when testing a method.

And this is the second greatest benefit of unit tests: the pain of setting up your dependencies forces you to analyze how you’re using them. And this analysis usually leads you to refactor your code to make it simpler and cleaner.

But unit tests can’t do it all, so until next time when I’ll write about acceptance tests and how they can really ramp up the quality of the code you produce.

-Julio

2 Comments

  1. Great article. I like the correlation between ease of testing and a better design.

    After dealing with mocks for a while, some people decry their usage at all because of what you said: our objects have so many dependencies that all of these mocks become a maintenance nightmare. I was once one of these people…

    The mocks were indeed causing pain, but it took me a while to realize the pain wasn’t the fault of the mocks themselves, it was due to a poor design. The mocks were doing their job by showing me that pain.

  2. Hi, interesting post. I wonder if the naming pattern of your test method is optimal. I mean, when unit testing, I think we should rather think in terms of class responsibility not method responsibility. So I wouldn’t put method name there. Also the “test_” prefix is not used anymore with JUnit4 so you can get rid of it. All in all, I would probably changed this
    test_methodToTest_ReturnsNullIfProvidedValueIsNull
    into
    shouldReturnNullIfProvidedValueIsNull

Leave a Reply

Your email address will not be published. Required fields are marked *