Chapter 10. Automate Tests

Keep the bar green to keep the code clean.

The jUnit motto

Guideline:

  • Automate tests for your codebase.

  • Do this by writing automated tests using a test framework.

  • This improves maintainability because automated testing makes development predictable and less risky.

In Chapter 4, we have presented isValid, a method to check whether bank account numbers comply with a checksum. That method contains a small algorithm that implements the checksum. It is easy to make mistakes in a method like this. That is why probably every programmer in the world at some point has written a little, one-off program to test the behavior of such a method, like so:

package eu.sig.training.ch10;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import eu.sig.training.ch04.v1.Accounts;

public class Program {
    public static void main(String[] args) throws IOException {
        BufferedReader isr =
            new BufferedReader(new InputStreamReader(System.in));
        String acct;
        do {
            System.out.println("Type a bank account number on the next line.");
            acct = isr.readLine();
            System.out.println("Bank account number '" + acct + "' is" +
                (Accounts.isValid(acct) ? "" : " not") + " valid.");
        } while (acct != null && acct.length() != 0);
    }
}

This is a Java class with a main method, so it can be run from the command line:

$ java Program
Type a bank account number on the next line.
123456789
Bank account number '123456789' is valid.
Type a bank account number on the next line.
123456788
Bank account number '123456788' is not valid.
$

A program like this can be called a manual unit test. It is a unit test because it is used to test just one unit, isValid. It is manual because the user of this program has to type in test cases manually, and manually assess whether the output of the program is correct.

While better than having no unit testing at all, this approach has several problems:

  • Test cases have to be provided by hand, so the test cannot be executed automatically in an easy way.

  • The developer who has written this test is focusing on logic to execute the test (the do … while loop, all input/output handling), not on the test itself.

  • The program does not show how isValid is expected to behave.

  • The program is not recognizable as a test (although the rather generic name Program is an indication it is meant as a one-off experiment).

That is why you should write automated unit tests instead of manual unit tests. These are tests of code units themselves described in code that runs autonomously. The same holds for other types of testing, such as regression tests and user acceptance tests: automate as much as possible, using a standard test framework. For unit tests, a common framework is jUnit.

Motivation

This section describes the advantages of automating your tests as much as possible.

Automated Testing Makes Testing Repeatable

Just like other programs and scripts, automated tests are executed in exactly the same way every time they are run. This makes testing repeatable: if a certain test executes at two different points in time yet gives different answers, it cannot be that the test execution itself was faulty. One can conclude that something has changed in the system that has caused the different outcome. With manual tests, there is always the possibility that tests are not performed consistently or that human errors are made.

Automated Testing Makes Development Efficient

Automated tests can be executed with much less effort than manual tests. The effort they require is negligible and can be repeated as often as you see fit. They are also faster than manual code review. You should also test as early in the development process as possible, to limit the effort it takes to fix problems.

Important

Postponing testing to a late stage in the development pipeline risks late identification of problems. That costs more effort to fix, because code needs to go back through the development pipeline and be merged, and tests must be rerun.

Automated Testing Makes Code Predictable

Technical tests can be automated to a high degree. Take unit tests and integration tests: they test the technical inner workings of code and the cohesion/integration of that code. Without being sure of the inner workings of your system, you might get the right results by accident. It is a bit like driving a car: you might arrive at an intended destination by following the wrong directions, but when you want to go to another destination, you are uncertain whether the new directions are reliable and will actually take you there.

A common advantage of automated testing is identifying when regression is occurring. Without a batch of automated unit tests, development quickly turns into a game of whack-a-mole: you implement a change in one piece of code, and while you are working on the next change in another piece of code, you realize you have introduced a bug with your previous change. Automated tests allow you to double-check your entire codebase effortlessly before turning to the next change. And since the automated unit tests follow predefined paths, you can be sure that if you have fixed a bug, it does not pop up on a second run.

Thus, running automated tests provides certainty about how the code works. Therefore, the predictability of automated tests also makes the quality of developed code more predictable.

Tests Document the Code That Is Tested

The script or program code of a test contains assertions about the expected behavior of the system under test. For example, as will be illustrated later in this chapter, an appropriate test of isValid contains the following line of code: assertFalse(isValid("")). This documents, in Java code, that we expect isValid to return false when checking the empty string. In this way, the assertFalse statement plays a double role: as the actual test, and as documentation of the expected behavior. In other words, tests are examples of what the system does.

Writing Tests Make You Write Better Code

Writing tests helps you to write testable code. As a side effect, this leads to code consisting of units that are shorter, are simpler, have fewer parameters, and are more loosely coupled (as the guidelines in the previous chapters advise). For example, a method is more difficult to test when it performs multiple functions instead of only one. To make it easier to test, you move responsibilities to different methods, improving the maintainability of the whole. That is why some development approaches advocate writing a unit test before writing the code that conforms to the test. Such approaches are called test-driven development (TDD) approaches. You will see that designing a method becomes easier when you think about how you are going to test it: what are the valid arguments of the method, and what should the method return as a result?

How to Apply the Guideline

How you automate tests differs by the types of tests you want to automate. Test types differ in what is tested, by whom, and why, as detailed in Table 10-1. They are ordered from top to bottom based on the scope of the tests. For example, a unit test has the unit as scope, while an end-to-end test, a regression test, and an acceptance test are on the system level.

Table 10-1. Types of testing
Type What it tests Why Who

Unit test

Functionality of one unit in isolation

Verify that unit behaves as expected

Developer (preferably of the unit)

Integration test

Functionality, performance, or other quality characteristic of at least two classes

Verify that parts of the system work together

Developer

End-to-end test

System interaction (with a user or another system)

Verify that system behaves as expected

Developer

Regression test

Previously erroneous behavior of a unit, class, or system interaction

Ensure that bugs do not re-appear

Developer

Acceptance test

System interaction (with a user or another system)

Confirm the system behaves as required

End-user representative (never the developer)

Table 10-1 shows that a regression test is a unit test, an integration test, or an end-to-end test that has been created when a bug was fixed. Acceptance tests are end-to-end tests executed by end user representatives.

Different types of testing call for different automation frameworks. For unit testing, several well-known Java frameworks are available, such as jUnit. For automated end-to-end testing, you need a framework that can mimic user input and capture output. A well-known framework that does just that for web development is Selenium. For integration testing, it all depends on the environment in which you are working and the quality characteristics you are testing. SoapUI is a framework for integration tests that focuses on web services and messaging middleware. Apache jMeter is a framework for testing the performance of Java applications under heavy workloads.

Choosing a test framework needs to be done at the team level. Writing integration tests is a specialized skill—but unit testing is for each and every individual developer. That is why the rest of this chapter focuses on writing unit tests using the most well-known framework for Java: jUnit.

Note

Contrary to specialized integration and end-to-end tests, writing unit tests is a skill that every developer needs to master.

Writing unit tests also requires the smallest upfront investment: just download jUnit from http://www.junit.org.

Getting Started with jUnit Tests

As we noted in the introduction of this chapter, we want to test isValid, a method of theclass Accounts. Accounts is called the class under test. In jUnit 4, tests are put in a different class, the test class. By convention, the name of the test class is the name of the class under test with the suffix Test added. In this case, that would mean the name of the test class is AccountsTest. It must be a public class, but apart from that, there are no other requirements for a test class. In particular, it does not need to extend any other class. It is convenient, but not required, to place the test class in the same package as the class under test. That way, the test class has access to all members of the test class under test that have package (but not public) access.

In jUnit 4, a test itself is any method that has the @Test annotation. By convention, the name of a test method starts with test, but this is just a convention, not a requirement. To test isValid, you can use the following jUnit 4 test class:

package eu.sig.training.ch04.v1;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

public class AccountsTest {

    @Test
    public void testIsValidNormalCases() {
        assertTrue(Accounts.isValid("123456789"));
        assertFalse(Accounts.isValid("123456788"));
    }

}

This test handles two cases:

  • Bank account number 123456789: We know this is a valid bank account number (see “The 11-Check for Bank Account Numbers”), so isValid should return true. The call of assertTrue tests this.

  • Bank account number 123456788: We know this is an invalid bank account number (because it differs from a valid account number by one digit), so isValid should return false. The call of assertFalse tests this.

Unit tests can be run directly in the IDE Eclipse. In addition, jUnit 4 comes with test runners to run tests from the command line. Tests can also be executed by Maven or Ant. Figure 10-1 shows the result of running the preceding test in Eclipse. The darker bar indicates that all tests have succeeded.

blms 1001
Figure 10-1. All tests succeeded!

The test in the preceding test class only tests normal cases: two bank account numbers of the expected format (exactly nine characters, all digits). How about corner cases? One obvious special case is the empty string. The empty string is, of course, not a valid bank account number, so we test it by calling assertFalse:

@Test
public void testEmptyString() {
    assertFalse(Accounts.isValid(""));
}

As Figure 10-2 shows, it turns out that this test fails! While the call to isValid should return false, it actually returned something else (which, of course, must be true, as there is no other option).

blms 1002
Figure 10-2. One test failed

The failed test points us to a flaw in isValid. In case the argument to isValid is the empty string, the for loop does not run at all. So the only lines executed are:

int sum = 0;
return sum % 11 == 0;

This indeed returns true, while it should return false. This reminds us to add code to isValid that checks the length of the bank account number.1

The jUnit 4 runner reports this as a test failure and not as a test error. A test failure means that the test itself (the method testEmptyString) is executed perfectly, but the assertion failed. A test error means that the test method itself did not execute correctly. The following code snippet illustrates this: the showError method raises a division-by-zero exception and never even executes assertTrue:

@Test
public void showError() {
    int dummy = 1 / 0;
    // Next line is never executed because the previous one raises an
    // exception.
    // If it were executed, you'll never see the assert message because
    // the test always succeeds.
    assertTrue("You will never see this text.", true);
}

Next, we present some basic principles that will help you write good unit tests. We start with the most basic principles and then progress to more advanced ones that apply when your test efforts become more mature.

General Principles for Writing Good Unit Tests

When writing tests, it is important to keep in mind the following general principles:

Test both normal and special cases

As in the examples given in this chapter, test two kinds of cases. Write tests that confirm that a unit indeed behaves as expected on normal input (called happy flow or sunny-side testing). Also write tests that confirm that a unit behaves sensibly on non-normal input and circumstances (called unhappy flow or rainy-side testing). For instance, in jUnit it is possible to write tests to confirm that a method under test indeed throws a certain exception.

Maintain tests just like nontest (production) code

When you adjust code in the system, the changes should be reflected in the unit tests as well. This is most relevant for unit tests, though it applies to all tests. In particular, when adding new methods or enhancing the behavior of existing methods, be sure to add new test cases that cover that new code.

Write tests that are isolated: their outcomes should reflect only the behavior of the subject being tested

That is, each test should act independently of all other tests. For unit testing, this means that each test case should test only one functionality. No unit test should depend on state, such as files written by other tests. That is why a unit test that, say, causes the class under test to access the filesystem or a database server is not a good unit test.

Consequently, in unit testing you should simulate the state/input of other classes when those are needed (e.g., as arguments). Otherwise, the test is not isolated and would test more than one unit. This was easy for the test of isValid, because isValid takes a string as an argument, and it does not call other methods of our system. For other situations, you may need a technique like stubbing or mocking.

In Chapter 6, we introduced a Java interface for a simple digital camera, which is repeated here for ease of reference:

public interface SimpleDigitalCamera {
    public Image takeSnapshot();

    public void flashLightOn();

    public void flashLightOff();
}

Suppose this interface is used in an application that ensures people never forget to turn on the flash at night:

public final static int DAYLIGHT_START = 6;

public Image takePerfectPicture(int currentHour) {
    Image image;
    if (currentHour < PerfectPicture.DAYLIGHT_START) {
        camera.flashLightOn();
        image = camera.takeSnapshot();
        camera.flashLightOff();
    } else {
        image = camera.takeSnapshot();
    }
    return image;
}

Although the logic is simple (takePerfectPicture simply assumes that if the hour of the day on a 24-hour clock is lower than 6 p.m., it is night), it needs testing. For a proper unit test for takePerfectPicture to be written, taking a picture needs to be automatic and independent. That means that the normal implementation of the digital camera interface cannot be used. On a typical device, the normal implementation requires a (human) user to point the camera at something interesting and press a button. The picture taken can be any picture, so it is hard to test whether the (supposedly perfect) picture taken is the one expected.

The solution is to use an implementation of the camera interface that has been made especially for testing. This implementation is a fake object, called a test stub or simply a stub.2 In this case, we want this fake object to behave in a preprogrammed (and therefore predictable) way. We write a test stub like this:

class DigitalCameraStub implements SimpleDigitalCamera {
    public Image testImage;

    public Image takeSnapshot() {
        return this.testImage;
    }

    public void flashLightOn() {}

    public void flashLightOff() {}
}

In this stub, takeSnapshot always returns the same image, which we can set simply by assigning to testImage (for reasons of simplicity, we have made testImage a public field and do not provide a setter). This stub can now be used in a test:

@Test
public void testDayPicture() throws IOException {
    BufferedImage image =
        ImageIO.read(new File("src/test/resources/VanGoghSunflowers.jpg"));
    DigitalCameraStub cameraStub = new DigitalCameraStub();
    cameraStub.testImage = image;
    PerfectPicture.camera = cameraStub;
    assertEquals(image, new PerfectPicture().takePerfectPicture(12));
}

In this test, we create a stub camera and supply it with an image to return. We then call takePerfectPicture(12) and test whether it returns the correct image. The value of the call, 12, means that takePerfectPicture assumes it is between noon and 1 p.m.

Now suppose we want to test takePerfectPicture for nighttime behavior; that is, we want to ensure that if takePerfectPicture is called with a value lower than PerfectPicture.DAYLIGHT_START, it indeed switches on the flash. So, we want to test whether takePerfectPicture indeed calls flashLightOn. However, flashLightOn does not return any value, and the SimpleDigitalCamera interface also does not provide any other way to know whether the flash has been switched on. So what to check?

The solution is to provide the fake digital camera implementation with some mechanism to record whether the method we are interested in gets called. A fake object that records whether expected calls have taken place is called a mock object. So, a mock object is a stub object with added test-specific behavior. The digital camera mock object looks like this:

class DigitalCameraMock implements SimpleDigitalCamera {
    public Image testImage;
    public int flashOnCounter = 0;

    public Image takeSnapshot() {
        return this.testImage;
    }

    public void flashLightOn() {
        this.flashOnCounter++;
    }

    public void flashLightOff() {}
}

Compared to DigitalCameraStub, DigitalCameraMock additionally keeps track of the number of times flashLightOn has been called, in a public field. DigitalCameraMock still contains preprogrammed behavior, so it is both a stub and a mock. We can check that flashLightOn is called in the unit test like so:

@Test
public void testNightPicture() throws IOException {
    BufferedImage image =
        ImageIO.read(new File("src/test/resources/VanGoghStarryNight.jpg"));
    DigitalCameraMock cameraMock = new DigitalCameraMock();
    cameraMock.testImage = image;
    PerfectPicture.camera = cameraMock;
    assertEquals(image, new PerfectPicture().takePerfectPicture(0));
    assertEquals(1, cameraMock.flashOnCounter);
}

In these examples, we have written our own stub and mock objects. This leads to a lot of code. Generally, it is most efficient to use a mocking framework such as Mockito or EasyMock. Mocking frameworks use features of the Java Virtual Machine to automatically create mock objects from normal interfaces or classes. They also provide methods to test whether methods of a mock object have been called, and with which arguments. Some mocking frameworks also provide ways to specify preprogrammed behavior of mock objects, giving them the characteristics of both stubs and mocks.

Indeed, using Mockito as an example, you can write testNightPicture without any need to write a class like DigitalCameraMock yourself:

@Test
public void testNightPictureMockito() throws IOException {
    BufferedImage image =
        ImageIO.read(new File("src/test/resources/VanGoghStarryNight.jpg"));
    SimpleDigitalCamera cameraMock = mock(SimpleDigitalCamera.class);
    PerfectPicture.camera = cameraMock;
    when(cameraMock.takeSnapshot()).thenReturn(image);
    assertEquals(image, new PerfectPicture().takePerfectPicture(0));
    verify(cameraMock).flashLightOn();
}

In this test, Mockito’s mock method is called to create cameraMock, the mock object used in this test. With Mockito’s when and thenReturn methods, the desired behavior is specified. Mockito’s verify method is used to verify whether flashLightOn has been called.

Measure Coverage to Determine Whether There Are Enough Tests

How many unit tests are needed? One way to assess whether you have written enough unit tests is to measure coverage of your unit tests. Coverage, or more precisely, line coverage, is the percentage of lines of code in your codebase that actually get executed when all unit tests are executed. As a rule of thumb, you should aim for at least 80% line coverage with a sufficient number of tests—that is, as many lines of test code as production code.

Why 80% coverage (and not 100%)? Any codebase contains fragments of trivial code that technically can be tested, but are so trivial that testing them makes little sense. Take the following typical Java getter method:

public String getName() {
    return this.name;
}

It is possible to test this getter (with something like assertEquals(myObj.getName(),"John Smith")), but with this test, you are mostly testing that the Java compiler and the Java Virtual Machine work as expected. But it is not true that you should never test getters. Take a typical class that represents postal mail addresses. It typically has two or three string fields that represent (additional) address lines. It is easy to make a mistake like this one:

public String getAddressLine3() {
    return this.addressLine2;
}

A minimum of 80% coverage alone is not enough to ensure high-quality unit tests. It is possible to get high coverage by testing just a few high-level methods (like main, the first method called by the Java Virtual Machine) and not mock out lower-level methods. That is why we advise a 1-to-1 ratio of production code versus test code.

You can measure coverage using a code coverage tool. Well-known examples are the Maven plugin Cobertura, and the Eclipse plugin EclEmma. Figure 10-3 shows coverage of the Joda codebase, an open source Java library with date and time classes that comes with many unit tests.

JPacman coverage report
Figure 10-3. Joda time coverage report in Eclipse, as provided by the EclEmma plugin.

Common Objections to Automating Tests

This section discusses typical objections and limitations regarding automation. They deal with the reasons and considerations to invest in test automation.

Objection: We Still Need Manual Testing

“Why should we invest in automated tests at all if we still need manual testing?”

The answer to this question is simply because test automation frees up time to manually test those things that cannot be automated.

Consider the downsides of the alternative to automated tests. Manual testing has clear limitations. It is slow, expensive, and hard to repeat in a consistent manner. In fact, technical verification of the system needs to take place anyway, since you cannot manually test code that does not work. Because manual tests are not easily repeatable, even with small code changes a full retest may be needed to be sure that the system works as intended.

Note

Manual acceptance testing can largely be automated with automated regression tests. With those, the scope of remaining manual tests decreases. You may still need manual review or acceptance tests to verify that business logic is correct. This typically concerns the process flow of a functionality.

Objection: I Am Not Allowed to Write Unit Tests

“I am not allowed to write unit tests because they lower productivity according to my manager.”

Writing unit tests during development actually improves productivity. It improves system code by shifting the focus from “what code should do” toward “what it should not do.” If you never take into account how the code may fail, you cannot be sure whether your code is resilient to unexpected situations.

The disadvantages of not having unit tests are mainly in uncertainty and rework. Every time a piece of code is changed, it requires painstaking review to verify whether the code does what it is supposed to do.

Objection: Why Should We Invest in Unit Tests When the Current Coverage Is Low?

“The current unit test coverage of my system is very low. Why should I invest time now in writing unit tests?”

We have elaborated on the reasons why unit tests are useful and help you develop code that works predictably. However, when a very large system has little to no unit test code, this may be a burden. After all, it would be a significant investment to start writing unit tests from scratch for an existing system because you would need to analyze all units again. Therefore, you should make a significant investment in unit tests only if the added certainty is worth the effort. This especially applies to critical, central functionality and when there is reason to believe that units are behaving in an unintended manner. Otherwise, add unit tests incrementally each time you change existing code or add new code.

Note

In general, when the unit test coverage of a system is much below the industry best practice of 80%, a good strategy is to apply the “Boy Scout rule.” This rule says to leave code in a better state than you found it (see also Chapter 12 on applying this principle). Thus, when you are adjusting code, you have the opportunity to (re)write unit tests to ensure that in the new state, the code is still working as expected.

See Also

Standardization and consistency in applying it are important in achieving a well-automated development environment. For elaboration, see Chapter 11.

1 And that is still not enough. Because java.char.getNumericValue returns 10 for A, 11 for B, and so forth, isValid("ABCDEFGK") returns true.

2 In textbooks and other resources about testing, there is little if any agreement on terminology. We adopt the terminology of The Art of Unit Testing by Roy Osherove (Manning Publications, 2009).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset