How Do We Test This – Unit Testing

For this post, I’d like to change things up a bit and talk specifics – in this case, unit testing. Many guides on unit testing tend to include code samples that I find overly simplistic, so I’d like to provide something that’s a bit closer to what we might actually encounter in the wild.

Let’s say we’re working with an existing code base and we’ve been given the task to add unit tests for an existing class. Not, maybe, the ideal situation, but the norm for most teams I’ve been on. Here is the class:

public class SampleClass {
	private SampleDependency dependency;

	public SampleClass(SampleDependency dependency) {
		this.dependency = dependency;
	}
	
	String sampleMethod(String s) {
		if(s == null) {
			throw new IllegalArgumentException(s);
		} else if(s.isEmpty()) {
			return "";
		}
		
		StringBuilder sb = buildRequest(s);
		return dependency.doSomething(sb.toString());
	}

	private StringBuilder buildRequest(String s) {
		StringBuilder sb = new StringBuilder(s);
		sb.append("foo=foo2");
		sb.append("bar=baz");
		return sb;
	}
}

Like code in many real projects, this class has a dependency. What that dependency is doesn’t matter too much here – it could be a database, an API, or some other internal class, but what matters is that to create a unit test, we want to test this class only. This means we need to isolate this class from it’s dependencies. This is one of the main ways we differentiate unit tests from other types of testing.

What is the process for doing this? If we create fake (mock) versions of our dependency and substitute them in, we could get them to act in any way we wanted, while still calling the real method in the class we want to test. While we could create them ourselves, we can instead use mocking frameworks that do the heavy lifting for us. In this example, we will be using Mockito.

String sampleMethod(String s) {
    if(s == null) {
        throw new IllegalArgumentException(s);
    } else if(s.isEmpty()) {
        return "";
    }
    ...
}

When analyzing our methods for testing, a good starting point is to consider all the different execution paths – happy paths for valid input and sad paths for invalid. The first part of our method includes some guard clauses against null and empty string. For our first tests, we will start by verifying these paths.

At first glance, this may not seem to add much value. We know passing in null will throw an IllegalArgumentException, so what value is there in adding a test that explicitly passes in null, just to check the thrown exception? Tests can act as a kind of documentation in code form. We can use our unit tests to capture the current behavior of the method, so that we can verify that any future changes made to it are actually intentional.

@Test
void testSampleMethodThrowsIllegalArgumentExceptionWhenStringIsNull() {
    assertThrows(IllegalArgumentException.class, () -> {
        sampleClass.sampleMethod(null);	
    });
}
	
@Test
void testSampleMethodReturnsEmptyStringWhenStringIsEmptyString() {
    String result = sampleClass.sampleMethod("");
    assertTrue(result.isEmpty());
}

These tests are low-hanging fruit. The more significant complexity comes when we look at the rest of the method.

        StringBuilder sb = buildRequest(s);
        return dependency.doSomething(sb.toString());
    }

    private StringBuilder buildRequest(String s) {
        StringBuilder sb = new StringBuilder(s);
        sb.append("foo=foo2");
        sb.append("bar=baz");
        return sb
    }

Now we’ve reached the code that calls our dependency and so we need to use Mockito to create a mock that we can use and pass to the class.

@Test
void testSampleMethodReturnsCorrectResult() {
    // Given - set up our precondition
    String expectedResult = "sample result";
    given(dependency.doSomething(anyString())).willReturn(expectedResult);
	
    // When - call our method
    String actualResult = sampleClass.sampleMethod("sample request");
	
    // Then - check the results
    assertEquals(expectedResult, actualResult);
}

Using Mockito’s given() method, we can make our mocked object return whatever we want. The ability to do this is aided by using dependency injection and storing an interface over an implementation. Once we’ve set up our mock, we can then call the methods we want to test using it.

But what about the private method? How do we test this when we don’t have any direct way to call it?

private StringBuilder buildRequest(String s) {
    StringBuilder sb = new StringBuilder(s);
    sb.append("foo=foo2");
    sb.append("bar=baz");
    return sb;
}

For this, we have two options. If the private helper method has only a single execution path, then we typically do not need to call the method directly to test it. We can rely on the tests for its calling methods to cover it. But, if there is additional complexity, e.g. conditionals, then instead, we should extract the method to a separate class, as indirect testing may not be capable of hitting all the execution paths . If we are not able to make changes to the class, then we have no choice but to fall back onto indirect means, which may not be able to test all execution paths.

The source code for this post can be found here.

One thought on “How Do We Test This – Unit Testing

Comments are closed.