koduki
Posted on September 30, 2018
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;
}
}
// UnitTest Code:
@Test
public void testMakeMessage() {
assertThat(Example01Good.makeMessage("world"), is("Hello, world"));
}
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);
}
}
// 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"));
}
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);
}
}
// 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)));
}
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;
}
}
}
}
// 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"));
}
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);
}
}
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);
}
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()));
}
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;
}
}
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"));
}
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);
}
}
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);
}
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)));
}
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);
}
}
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)));
}
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.
Posted on September 30, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.