DEV Community

koduki
koduki

Posted on

How do you write "Production Code" for UnitTest?

Introduction

UnitTest is very important for keeping code quality.
So almost project require to write UnitTest.

As a result, I will review strange UnitTest or I will be asked them about "I can't write UnitTest!".

Why? Because they don't consider about production code for UnitTest.
They only consider about UnitTest code for UnitTest. This is not good.

We should divid "business logic" and "uncontrolled value" from production code for writing UnitTest. This is also good for "readability".

Basic rule is only two.

  • Sepalate "business logic" and "uncontrolled value like a I/O, date, random"
  • Inject dependency by argument, constructer, non-private fields

In this article, I explain how do you write production code for UnitTest.

Typically example

Firstly, I explain typically cases.

Case 01: "I want to output to standard output"

You should sepalate the logic and the output method like a System.out.println or Logger.info.
You make a simple method for building "output string".

// Production 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;
    }
}
Enter fullscreen mode Exit fullscreen mode
// UnitTest Code:

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

Case 02: "I want to use random number"

Random number is not able to fix result.
So we need to sepalate the logic and the number generator.
This is a simple dice game logic for checking "Odd" or "Even"

// Production Code:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode
// Test Code:

@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"));
}
Enter fullscreen mode Exit fullscreen mode

Case 03: "I want to calcurate calendar like tomorrow, 1 year ago or later"

Calculating calendar is very general code. But current time like today is uncontrolled value.
You need to sepalate the calendar Calculation logic and getting current time.

You can use method arguments like a random number. This is good pracitice.
But sometimes getting current time is used on many places. In such a case, you can also use factory pattern.

// Production Code

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

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

    public LocalDate tomorrow2() {
        return systemDate.today().plusDays(1);
    }
}
Enter fullscreen mode Exit fullscreen mode
// Test Code

@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)));
}
Enter fullscreen mode Exit fullscreen mode

Case 04: "I want to manage File I/O"

Basically, this is same with Standard I/O case. You should sepalate the logic for building Text or Binary and the logic for read or write.
This is pretiy good.

But sometimes you need very many text size, checking line order and so on.
In such a case, you can use Reader/Writer and InputStream/OutputStream to sepalate logic and I/O.

// Production Code

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;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
// Test Code

@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"));
}
Enter fullscreen mode Exit fullscreen mode

Fundamental Concept

In this section, I explain more deeply.

Sepalate logic and I/O

Most importantly, you should seplate logic and I/O every time.
This is important desigin for UnitTest.

Let's take an example of a simple program for "getting text from command line arguments and print out to standard out".

public class Example01Bad {
    public static void main(String[] args) {
        String message = args[0];
//         String message = "World"; // for debug.
        System.out.println("Hello, " + message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Maybe, this is the most simple code. I beleave you write down same code firstly.
for debug comment is funny.

Next, let's write UnitTest.

/**
 * Super bad code
 */
@Test
public void testMain() {
    String[] args = {"world"};
    Example01Bad.main(args);
}
Enter fullscreen mode Exit fullscreen mode

This is a super simple. But there is NO assertion! So this UnitTest require to check True or False by your eyes!
Do you think this is a joke? Unfortunatly, I see such a UnitTest on real project again and again...

Of cource this code is terrible. More better people write blow code.

/**
 * Bad code
 */
@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()));
}
Enter fullscreen mode Exit fullscreen mode

This code hook StandardOut. It is not bad.
You can run it as UnitTest perfectly. But it's complex jsut a little.

If you can't modify target production code, you should write it.
However if you can change production code, UnitTest becomes more easily.

// Good production 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

I sepalate "building message logic" as makeMessage method. So UnitTest becomes following.

// Good test code
@Test
public void testMakeMessage() {
    assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}
Enter fullscreen mode Exit fullscreen mode

This is a very simple. And it's perfect as UnitTest.

You might think it strange, because this code doesn't do any test about StandardOut.
Exactly. But it's not necessary as UnitTest.

Basically, Checking bussiness logic is the most impotantly in UnitTest.
Sysmte.out.println and Logging library is standard or popular library.
That means their code quality is keeped by other tests. You are only careful about your business logcic.
Of course, you also need to check about Standard Output and so on. But it is integration test.

Don't initilze uncontrolled values directly

You should not initialize uncontrolled value like a randome number, date, RPC(WebAPI), DAO on your each method.
Please apply the concept of DI(Dependency Injection).

Let's take an example of a method for calculating tomorrow.

public class Example03Bad {
    public LocalDate tomorrow() {
        return LocalDate.now().plusDays(1);
    }
}
Enter fullscreen mode Exit fullscreen mode

Naturally, this code is not able to do UnitTest. Because LocalDate.now() value changes every time.
So LocalDate.now() is uncontrolled value. This is a typical beginner's trap.
You need to sepalate it from business logic. The most simple solution is to use method arguments.

public LocalDate tomorrow(LocalDate today) {
    return today.plusDays(1);
}
Enter fullscreen mode Exit fullscreen mode

So you can fix test code follwing.

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

This is enough. But if getting current time is used on many places, you can also factory method pattern and stub.
Firstly, you make factory class which has a method today() to return LocalDate.
Next, you set it on target code field.

Return value of today() is depends on implementation. In production, it 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

In test code, you use stub instead of LocalDate.now().

@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)));
}
Enter fullscreen mode Exit fullscreen mode

The key is to use package scope instead of private scope or you should initialize by constructor.
If you use private scope, you need a mock framework and so on.

Summary

Basic rule is only two.

  • Sepalate "business logic" and "uncontrolled value like a I/O, date, random"
  • Inject dependency by argument, constructer, non-private fields

This is not difficult. But sometimes biginner doesn't know it.
In addtion, TDD(Test Driven Development) and Test First force you to write such code.
It is a reasn that TDD is populer.

And functional language is more strict style about side effect. Let's try to study also it.

Latest comments (2)

Collapse
 
dariusx profile image
Darius

Also, keeping process/logic independent of specific input/output type is useful beyond just the ability to Unit Test. It's as old as the COBOL days of Jackson Diagrams, and its the underpinning of multiple "layers". The same pattern is useful at the lower-level of a specific program or class.

Collapse
 
koduki profile image
koduki

Definitely, I think software engineers have developed many technic to keep process/logic independent.
MVC, DI, layered architectures(Maybe OSI reference model is the most famous) and so on.

It's not special things, but it's important in any era.