[JAVA] Only this I want to remember, 4 designs for writing unit tests

I also summarized a little more convention in "I tried to organize the UT creation criteria in Java" ..

Introduction

Last year, I wrote an article on my blog called "7 rules for normal unit testing". There was more response than I expected. What I wrote in that article is just the principle / principle, and some techniques are required to realize it.

In particular, even if you make such a rule and strictly adhere to "writing a unit test" Without knowing the proper technique, maintenance may be difficult, quality may not be contributed, and garbage with poor execution performance may be mass-produced.

Then, what to do is "design the original code so that unit tests are easy to write from the beginning". so. The first thing you should learn is not "how to write test code" but "test target code", that is, "how to write product code". Also, the "from the beginning" mentioned here is a level that you can do it properly before giving it to people such as reviews and PR merging without thoroughly saying "Test first! Do TDD!" ..

However, since it is a slight change of thinking, I think that it is easier for beginners to see a concrete example, so I made a typical example as a cheat sheet.

Design cheat sheet

The basic idea is to "separate business logic and I / O" and "return the same value no matter how many times it is repeated". The following is a concrete example based on that idea.

I want to create a program that outputs standard output

Test a method that simply returns a string, separating the output, such as System.out.println and Logger.info, from the "assemble the output" part.

Product code example:

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }
}

Test code example:

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

I want to create a method that handles random numbers

Since the value of a random number changes every time, pass the value as an argument separately from the business logic. A program that determines whether the dice are even or odd looks like this.

Product code example:

public class Example02Good {
    public static void main(String[] args) {
        System.out.println(check(throwDice(Math.random())));
    }

    static String check(int num) {
        return (num % 2 == 0) ? "Win" : "Lose";
    }

    static int throwDice(double rand) {
        return (int) (rand * 6);
    }
}

Test code example:

@Test
public void testCheck() {
    assertThat(Example02Good.check(1), is("Lose"));
    assertThat(Example02Good.check(2), is("Win"));
    assertThat(Example02Good.check(3), is("Lose"));
    assertThat(Example02Good.check(4), is("Win"));
    assertThat(Example02Good.check(5), is("Lose"));
    assertThat(Example02Good.check(6), is("Win"));
}

I want to create a method that handles dates such as the calculation of the next day

Make it given from the outside as in the case of random numbers. As another solution, as in the example, create a factory for date generation and use a dummy (stub) for testing.

Product code example:

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

Test code example:

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

I want to handle file input / output

Basically, file input / output should be a method that handles character strings, but There are cases where the actual data is very large or it must be processed as "rows". In that case, you can actually verify the reading and writing of the file, but since the description cost and execution cost are high, Separate the direct handling of files from within the logic, such as Reader / Writer and InputStream / OutputStream It is easy to program the interface and use StringReader / StringWriter etc. for testing.

Product code example:

public class Example04Good {
    public static void main(String[] args) throws Exception {
        System.out.println("hello");
        try (Reader reader = Files.newBufferedReader(Paths.get("intput.txt"));
                Writer writer = Files.newBufferedWriter(Paths.get("output.txt"));) {
            addLineNumber(reader, writer);
        }
    }

    static void addLineNumber(Reader reader, Writer writer) throws IOException {
        try (BufferedReader br = new BufferedReader(reader);
                PrintWriter pw = new PrintWriter(writer);) {
            int i = 1;
            for (String line = br.readLine(); line != null; line = br.readLine()) {
                pw.println(i + ": " + line);
                i += 1;
            }
        }
    }
}

Test code example:

@Test
public void testAddLineNumber() throws Exception {
    Writer writer = new StringWriter();
    addLineNumber(new StringReader("a\nb\nc\n"), writer);
    writer.flush();
    String[] actuals = writer.toString().split(System.lineSeparator());

    assertThat(actuals.length, is(3));
    assertThat(actuals[0], is("1: a"));
    assertThat(actuals[1], is("2: b"));
    assertThat(actuals[2], is("3: c"));
}

Commentary or basic idea

Why write that, not just a concrete example? I will also explain that.

Separation of logic and I / O

Again, the basics are the separation of logic and I / O. Here are some tips for testing. Let's take "a program that processes characters received from command line arguments and outputs them as standard" as an example.

public class Example01Bad {
    public static void main(String[] args) {
        String message = args[0];
//         String message = "World"; //For operation check
        System.out.println("Hello, " + message);
    }
}

Isn't this the first code that many people write? The commented out code is nice. Let's write a unit test for this program.

@Test
public void testMain() {
    String[] args = {"world"};
    Example01Bad.main(args);
}

Is it like this? There is no assert! So, whether this program is normal or not is ** "humans have to judge visually" ** even though it is JUnit.

You might think, "Nobody writes this kind of code, www", but I've seen this kind of "test code that just works" many times with more complicated business logic.

Well, this is out of the question, but if you're a little more conscious, write:

/**
 *Highly conscious system useless test
 */
@Test
public void testMainWithSystemOut() {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    System.setOut(new PrintStream(out));

    String[] args = {"world"};
    Example01Bad.main(args);

    assertThat(out.toString(), is("Hello, world" + System.lineSeparator()));
}

The standard output is hooked and the results are compared. This test properly meets the requirements of the test, and while nothing is wrong, it is simply uselessly complicated. So I'll make it simpler by modifying the original code.

public class Example01Good {
    public static void main(String[] args) {
        String message = args[0];
        System.out.println(makeMessage(message));
    }

    static String makeMessage(String message) {
        return "Hello, " + message;
    }

"Logic to process character string" was cut out as makeMessage. Then the unit test looks like this.

@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}

It's become very simple, isn't it? It feels good.

However, some people may feel that "I haven't been able to test the function of displaying characters on the screen!" That's right. However, there is no need to confirm such a thing by unit test.

The most important unit test to focus on is logic testing. You don't need to do quality-checked tests like file I / O, standard I / O, or log output.

If you make such a file IO etc. and its quality is not guaranteed, Since the quality should be guaranteed by testing the file IO created by yourself, it is not necessary to check it by individual processing that uses the function. Testing in such a combination is also important, but it is carried out in another phase such as integration test.

Therefore, it is important to always be aware of creating business logic that simply returns the return value by breaking down the function as small as possible like this time. It's a good idea to push out I / O and uncontrollable value initialization as much as possible to the controller layer, and remove the tests there from the UT that runs every build. For maintenance of legacy code, it may be unavoidable to hook the standard output as shown in the bad example, but it is unnecessary if you write it yourself.

Do not directly initialize things for which you cannot control the value

It's also important not to initialize uncontrollable things like dates, random numbers or networks (like calling WebAPI) in the method under test, as a typical example.

For example, create a tomorrow method that asks for tomorrow.

public class Example03Bad {
    public LocalDate tomorrow() {
        return LocalDate.now().plusDays(1);
    }
}

Of course, if you initialize LocalDate directly in the tomorrow method like this, the value will change every day, so automatic testing is impossible.

This is a pretty serious problem, and some people say, "I don't know how to write unit tests," because I haven't properly eliminated these dependencies.

The simplest solution is to pass a date as an argument and separate it from the business logic.

public LocalDate tomorrow(LocalDate today) {
    return today.plusDays(1);
}

Then the test can be written with a fixed value like this.

@Test
public void testTomorrow() {
    Example03Good target = new Example03Good();
    assertThat(target.tomorrow(LocalDate.of(2017, 1, 16)), is(LocalDate.of(2017, 1, 17)));
}

Basically this is fine, but sometimes it's easier to use the Factory Method pattern and stubs when there are a lot of them. First, create a SystemDate class that generates LocalDate, use LocalDate.now in it, and set it in the field of Example03Good. Since the business logic gets the LocalDate via the SystemDate class, what is returned depends on the implementation. The product code is LocalDate.now.

class SystemDate {
    public LocalDate today() {
        return LocalDate.now();
    }
}

public class Example03Good {
    SystemDate systemDate = new SystemDate();

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}

The test code overwrites the systemDate with a stub like this:

@Test
public void testTomorrow2() {
    Example03Good target = new Example03Good();
    target.systemDate = new SystemDate() {
        @Override
        public LocalDate today() {
            return LocalDate.of(2017, 1, 16);
        }
    };
    assertThat(target.tomorrow2(), is(LocalDate.of(2017, 1, 17)));
}

The trick here is to place Factory fields like SystemDate at the package level or higher, not private. Then you can switch implementations without using a DI container or Mock framework. Of course, it's okay to make it private and pass it in the constructor, or pack it in the setter, but in the end it's still possible to change it, so it's just better to be easy.

Summary

Well, I wrote it briefly, but how is it? I hope you found that a little design ingenuity makes the test much easier to write.

It's not difficult, but I often make the same point to beginners, and I didn't know the material that summarizes from this point of view, so I wrote it. By the way, it is said that "TDD (Test Driven Development) contributes to quality" because this kind of writing is naturally forced.

Also, when studying functional languages, there are some points that seem to be the culmination of such design methods, so I think there are many points that can be helpful.

Well then, Happy Hacking!

Recommended Posts

Only this I want to remember, 4 designs for writing unit tests
I want to write a unit test!
[Rails] How to implement unit tests for models
I want to apply ContainerRelativeShape only to specific corners [SwiftUI]
I want to create a generic annotation for a type
I want to randomly generate information when writing test code
I want to return multiple return values for the input argument
I want to convert characters ...
I want to recursively search for files under a specific directory
I want to give edit and delete permissions only to the poster
I want to create a chat screen for the Swift chat app!
The story of Collectors.groupingBy that I want to keep for posterity
Swift: I want to chain arrays
I want to convert InputStream to String
Read H2 database for unit tests
I want to docker-compose up Next.js!
[Ruby] I want to output only the odd-numbered characters in the character string
I want to display the number of orders for today using datetime.
When you want Rails to disable a session for a specific controller only
Glassfish tuning list that I want to keep for the time being
[Ruby] I want to extract only the value of the hash and only the key
[Rspec] Flow from introducing Rspec to writing unit test code for a model
I want you to use Enum # name () for the Key of SharedPreference