標準出力に結果を出すプログラムを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を引数で渡すようにリファクタリングするという解法。確かにこれも、上記の例の場合は一つの方法ですね。