Unit Testing – a quick start guide to JUnit

JUnit is the de facto standard framework for writing Java unit tests. As scala is a language that creates Java classes you can also write JUnit tests for scala classes, either in scala or Java itself.

A quick way to get started would be to download eclipse for Java developers from here (you will need Java installed).

Once downloaded, open eclipse and create a new Java Project called Test, accepting the defaults.

Expand the Test project and right-click on the src folder, choosing New and then JUnit Test Case. You may need to filter the options to find JUnit and JUnit Test Case.

In the next dialog enter TestString for the Name and com.test for the package. You should be prompted whether to add JUnit to the project build path, choose Yes.

Depending on any options for code generation, you should now have a new class that looks roughly like this:

package com.test;

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

public class StringTest {

	@Test
	public void test() {
		fail("Not yet implemented");
	}
}

When we run this class JUnit will automatically invoke the test() method because it has a @Test annotation. Let’s try it – right-click the StringTest class and choose Run As -> JUnit Test.

The failure in the JUnit panel should contain a failure trace that looks something like this:

java.lang.AssertionError: Not yet implemented
	at com.test.StringTest.test(StringTest.java:11)

This is because we have a line in out test saying ‘fail’. If we just remove this we will have an empty test that passes, but where’s the fun in that. Let’s make an assertion instead. Replace the method contents to look like this:

@Test
public void test() {
  String name = "Vinny";
  assertEquals(5, name.length());
}

Now when we run it there is no failure trace, because the length (number of characters) of the String “Vinny” is 5. Change the number from 5 to a different integer value and the test will fail. This is our first JUnit test, we didn’t create our own class to test but did test something, namely the ‘length()’ method of the String class.

By convention we would have a test class in a folder called src/test/java. Typically we would have the package of our Test class (what we call a TestCase) in the same package as the class being tested.

Advertisements

Layers of abstraction in Unit Tests: Have an Implementation layer

Summary

Differing levels of abstraction can aid or hinder the task of maintaining unit tests.

Disclaimer

This idea is still a work in progress, the product of working on one set of tests on one project but even if this just helps people look at JUnit tests from a fresh angle my time writing this post will have been worthwhile (I tell myself).

Problem

JUnit tests in Java are normal classes which either extend TestCase (JUnit 3) or are annotated as tests (JUnit 4). As they are Java classes, this allows us to use inheritance and to abstract out some or all of the details. Too little abstraction in our test cases can result in a test case where it is almost impossible to see what is being set up and what is being verified, there are lots of lines of code and these will typically need to be commented to show which block starts where. Too much abstraction can lead to the same problem but due to a lack of structure in the class at hand.

In a nutshell my requirements story is this:

As a developer maintaining Unit Tests, I want to see in code the prerequisites, the method invocation and verifications, with a layer of abstraction that hides low-level concerns but contains enough of the domain to see what is happening.

Too much detail:

public class LoginTest {

  @Test
  public void testLogin() {
    // Set up login request, mock authenticator and * inject into the LoginService.
    final LoginRequest loginRequest = new LoginRequest();
    final String username = “testuser”;
    final String password = “testpass”;
    loginRequest.setUsername(username);
    loginRequest.setPassword(password);
    MockAuthenticator mockAuthenticator = new MockAuthenticator();
    mockAuthenticator.setValidCredentials(username, password);
    loginService.setAuthenticator(mockAuthenticator);

    // Call login method being tested:
    final LoginResult result = loginService.login(loginRequest);

    // Verify the result 
    Assert.assertTrue(result.isSuccessful);
    }
}

Not enough detail:

public class LoginTest extracts AbstractSuccessfulLoginServiceTest {
  // Sadly, there are test classes like this and they will fail
  protected LoginRequest createRequest() {
    return new SuccessfulLoginRequest();
  }
}

Solution

Make the test class copy the story or script and push everything else into an implementation layer. This is akin to the Fixture classes sometimes used, the more this is a complete layer supporting the TestCase classes the better, it is very easy for supporting classes to become disjointed and hard to implement a coherent strategy across the board.


public class LoginTest {
  @Test
  public void testLogin() {
    // Make the login service use a mock authenticator which generates a valid username/password for us.
    MockAuthenticator mock = new MockAuthenticator();
    final LoginRequest request = mock.createGoodRequest();
    loginService.setAuthenticator(mock);

    // Call login method being tested:
    final LoginResult result = loginService.login(request);

    // Verify the result
    Assert.assertTrue(result.isSuccessful);
    }
}

Notes

If our unit test classes are just normal classes and we are trying to write good code – remove duplication, etc – why are we not abstracting out to the nth degree? We/I would do this with non-test code. The difference is that each unit test is an interface to the system. Where we run a system/application as its own collection of components, with a unit test each test is a discrete entity with its own functional requirements. At least sometimes, we can’t/shouldn’t allow too much interdependence between these classes. They have to be able to stand on their own. The implementation of what they wish to be done, creating datasets, setting up mocks etc, this can be abstracted out but a top level dictating what it is to abstract out, that should remain.

DSLs

Test classes are a great place to use a Domain Specific Language. Where a number of tests rely on a rich layer to setup or verify tests this can be successfully aided by a DSL-like structure, with lines like the following:


Verify.that(loginResult).isSuccessful();
Verify.that(outputFile).contains(domainModels);

We are now working with the problem domain in a language specific to the problem. For more information on DSLs please see
this article.

Test Driven Development

Test Driven Development typically means when the test code is written first. This can be a lot of fun but doesn’t fit every scenario – when the code you are writing may not be part of the finished product, time spent writing tests can be a gamble.

A perfect fit for Test Driven Development is when writing functions with a few different paths through the code: there are a certain number of key paths through the code and these can be written out as tests, ready to be run with each modification to the code.

Let’s see how an example might work:

Imagine an online store. We have a need for a method to choose where a user will be redirected based on his login attempt but the logic is complex. If his login is unsuccessful he will go to ‘bad-login’, for successful logins he will go to ‘good-login’ unless he has items in his basket. If this is the case he will go to ‘basket’… unless he has 1 or more notifications, in which case he will go to ‘notifications.’ The method will take a UserDto and return a String.

We can write these requirements as tests and make sure that, however complex, we can cover our key scenarios and run through them in less than a second by running our test class. In this example I’ll use Scala to help anyone looking to see the direction in which Java is heading. I’ll use JUnit for the tests as Scala-Java integration is fantastic and lets a Java developer keep a lot of their favourite Java frameworks while moving to Scala.

package vff.blog
import junit.framework.TestCase
import org.junit.Assert
case class UserDto(username: String, hasNotifications: Boolean, hasItemsInBasket: Boolean) {
  // line above declares the class and constructor together - each will be immutable.
}

object RedirectService {
  // object as opposed to class creates a singleton
  val LOGIN_UNSUCCESSFUL = "bad-login"
  val LOGIN_SUCCESSFUL = "good-login"
  val SHOW_BASKET = "basket"
  def redirect(userDto: UserDto, loginSuccessful: Boolean): String = {
     "" // return an empty string
  }
}

This is my basic UserDto class holding a String and 2 Booleans and my RedirectService, a singleton (note use of the word object, not class) with a function taking a UserDto and a Boolean and returning a String. For now it returns an empty String – just enough to compile and run. For our first test we will create a ‘happy path’ test – I’d say the simplest path (not necessarily ‘happy’) is login unsuccessful (nothing else matters, return a string) so we’ll code that first.

package vff.blog

import org.junit.Assert
import junit.framework.TestCase
class RedirectServiceTest extends TestCase {
  def testLoginUnsuccessful {
    val userDto = new UserDto("vincefleetwood", false, false)
    val result = RedirectService.redirect(userDto, false)
    Assert.assertEquals(RedirectService.LOGIN_UNSUCCESSFUL, result)
  }
}

When we run this test we get a failure (as expected). Time to write some code to make our test pass. We change our method as shown:

def redirect(userDto: UserDto, loginSuccessful: Boolean): String = {
  LOGIN_UNSUCCESSFUL
}

Now our method always returns ‘bad-login’. Well, it’s a start 🙂 We can now run our test and it will pass. This might not feel like a big step but we’ll get there. Time for a second test, that when the user logs in without notifications or basket items they go to ‘good-login’:

Lets add the test first:

def</strong> testLoginSuccessfulBasic {
    <strong>val</strong> userDto = new UserDto("vincefleetwood", false, false)
    <strong>val</strong> result = RedirectService.redirect(userDto, true)
    Assert.assertEquals(RedirectService.LOGIN_SUCCESSFUL, result)
  }

We add the new constant to the Redirect Service (match is like a switch that can run on any value) and our test runs but fails.

We now conditionally return the new value:

  def redirect(userDto: UserDto, loginSuccessful: Boolean): String = {
    loginSuccessful  match {
      case true => LOGIN_SUCCESSFUL
      case false => LOGIN_UNSUCCESSFUL
    }
  }

We now write the test for items in the basket:

def testLoginSuccessfulItemsInBasket {
    val userDto = new UserDto("vincefleetwood", false, hasItemsInBasket = true)
    val result = RedirectService.redirect(userDto, true)
    Assert.assertEquals(RedirectService.SHOW_BASKET, result)
}

We also write the code to pass the test – refactoring a little what we have already written but with tests to make sure it still works:

object RedirectService { // object as opposed to class creates a singleton
  val LOGIN_UNSUCCESSFUL = "bad-login"
  val LOGIN_SUCCESSFUL = "good-login"
  val SHOW_BASKET = "basket"
  def redirect(userDto: UserDto, loginSuccessful: Boolean): String = {
    loginSuccessful match {
      case true if (userDto.hasItemsInBasket) => SHOW_BASKET
      case true                               => LOGIN_SUCCESSFUL
      case false                              => LOGIN_UNSUCCESSFUL
    }
  }
}

I’m guessing this is enough for now and leave the final test as an exercise for the reader. Please leave any comments/questions.

Unit testing – why? Part 2: What did I just break?

Ok, so I had to modify an existing class for a new feature. Does everything else still work ok?

If I have unit tests in place I can fix a lot of bugs without deploying, not just bugs in my new method but bugs in existing methods. If old code relies on a piece of code and it changes, this can break the old code. The worst example of this is fixing existing code when other code relied on it not working properly.

Having unit tests in place helps a developer see side effects of modifying existing code without the need to build, deploy and test.