標準出力に結果を出すプログラムをJUnit 4.1でテストする方法
「標準出力に結果を出すプログラムをJUnit 4.1でテストする方法」について書きます。
テスト対象となるプログラム(Hello.java)
たとえば、以下のようなプログラムHello.javaがあったとしましょう。
public class Hello {
public static void main(String[] args) {
System.out.println("Hello!");
System.out.println("http://www.hyuki.com/");
}
}これは次のようにすれば、普通にコンパイルと実行ができます。
C:\work> javac -version Hello.java javac 1.5.0_06 C:\work> java Hello Hello! http://www.hyuki.com/
懸念事項
テスト(ここではJUnit 4.1を使ったユニットテストのこと)をする場合、以下のことが気になります。
- 1. assertEquals(expected, actual)でテストするけれど、標準出力に流れてしまうactualのほうはどうやって得る?
- 2. プラットホームで改行コードが違うのをどう吸収する?
解決案
解決案としてこう考えました。
- 1. System.setOutを使って「ByteArrayOutputStreamにリダイレクト」させる
- 2. System.getProperty("line.separator")を使えばよい
テスト(TestHello.java)
以下がテストです。JUnit 4.1らしく、アノテーションを使います。
import static org.junit.Assert.assertEquals;
import junit.framework.JUnit4TestAdapter;
import org.junit.*;
import java.io.*;
public class TestHello {
private ByteArrayOutputStream _baos;
private PrintStream _out;
@Before public void setUp() {
_baos = new ByteArrayOutputStream();
_out = System.out;
System.setOut(
new PrintStream(
new BufferedOutputStream(_baos)
)
);
}
@After public void tearDown() {
System.setOut(_out);
}
@Test public void testHello() {
Hello.main(new String[0]);
System.out.flush();
String expected = joinStrings("Hello!", "http://www.hyuki.com/");
String actual = _baos.toString();
assertEquals(expected, actual);
}
private String joinStrings(String... strs) {
String newLine = System.getProperty("line.separator");
String result = "";
for (String s : strs) {
result += s + newLine;
}
return result;
}
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(TestHello.class);
}
}
コンパイルとテスト結果(JUnit 4.1)
JUnit 4.1は C:/junit4.1/ にインストールしてあるものとします。
C:\work> javac -version -classpath ".;C:/junit4.1/junit-4.1.jar" TestHello.java javac 1.5.0_06 C:\work> java -classpath ".;C:/junit4.1/junit-4.1.jar" org.junit.runner.JUnitCore TestHello JUnit version 4.1 . Time: 0.01 OK (1 test)
おお、うまくいっているようです。
テスト結果(JUnit 4.1)
ついでに、JUnit 3.8.1のjunit.textui.TestRunnerでも動くかどうか試してみます。
C:\work> javac -classpath ".;C:/junit4.1/junit-4.1.jar" TestHello.java C:\work> java -classpath ".;C:/junit4.1/junit-4.1.jar;C:/junit3.8.1/junit.jar" junit.textui.TestRunner TestHello . Time: 0.03 OK (1 test)
おお、うまくいっているようです。
お知恵拝借
みなさまのお知恵拝借。
- 標準出力のテストはこれで妥当でしょうか?
- これってEclipseでも使えるんでしょうか?(まだ試してない)
- JUnit 3.8.1のjunit.swingui.TestRunnerだと動かない。なぜでしょう?(まだ追っていない)
C:\work> java -classpath ".;C:/junit4.1/junit-4.1.jar;C:/junit3.8.1/junit.jar" junit.swingui.TestRunner TestHello
Exception in thread "main" java.lang.NoSuchMethodError: junit.runner.BaseTestRunner.inVAJava()Z
at junit.swingui.TestRunner.createUseLoaderCheckBox(TestRunner.java:258)
at junit.swingui.TestRunner.createUI(TestRunner.java:380)
at junit.swingui.TestRunner.start(TestRunner.java:702)
at junit.swingui.TestRunner.main(TestRunner.java:52)
追記
t-wadaさんから反応がありました。ありがとうございます。
ここで紹介されているのは…なんというかすごい方法ですね。
(t-wadaさんによる)
@Test public void 標準出力に2行出力すること() throws Exception {
// Arrange
final List<String> actual = new ArrayList<String>();
PrintStream fakePrintStream = new PrintStream("ファイル名はここではどうでもいい") {
@Override
public void println(String str) {
actual.add(str);
}
};
// Act
System.setOut(fakePrintStream);
Hello.main(null);
// Assert
List<String> expected = Arrays.asList("Hello!", "http://www.hyuki.com/");
assertEquals(expected, actual);
}ここでは「出力された文字列が何か」ではなく「printlnが何回どういう引数で呼び出されたか」という視点からのテストなのですね。
それはそれとして、
- 日本語のメソッド名
- "ファイル名はここではどうでもいい"っていうファイル名
- anonymous classで@Overrideを付ける
…って新鮮ですね。
追記
Hello.main(null)よりも、Hello.main(new String[0])のほうが適切だと気づいたので直しました。
追記
hiuchidaさんからもコメントをいただきました。ありがとうございます。
ええと、なるほど、なるほど。hiuchidaさんのアイディアを元に書き直してみました。
import static org.junit.Assert.assertEquals;
import junit.framework.JUnit4TestAdapter;
import org.junit.*;
import java.io.*;
public class TestHello {
private PrintStream _saved;
private ByteArrayOutputStream _actual;
private ByteArrayOutputStream _expected;
private PrintStream _out;
@Before public void setUp() {
_saved = System.out;
_actual = new ByteArrayOutputStream();
_expected = new ByteArrayOutputStream();
System.setOut(new PrintStream(new BufferedOutputStream(_actual)));
_out = new PrintStream(new BufferedOutputStream(_expected));
}
@After public void tearDown() {
System.setOut(_saved);
}
@Test public void testHello() {
// Expected
_out.println("Hello!");
_out.println("http://www.hyuki.com/");
_out.flush();
// Actual
Hello.main(new String[0]);
System.out.flush();
// Compare
assertEquals(_expected.toString(), _actual.toString());
}
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(TestHello.class);
}
}実行結果です。
C:\work> javac -version -classpath ".;C:/arc/junit.org/junit4.1/junit-4.1.jar" TestHello.java javac 1.5.0_08 C:\work> java -classpath ".;C:/junit4.1/junit-4.1.jar" org.junit.runner.JUnitCore TestHello JUnit version 4.1 . Time: 0.015 OK (1 test)
追記
Mock Objectを使った別の方向の展開も。
以下は、出力結果をファイルに用意して置くという解法。なるほど。
以下は、そもそもテスト対象コードにPrintStreamを引数で渡すようにリファクタリングするという解法。確かにこれも、上記の例の場合は一つの方法ですね。