結城浩のはてなブログ

ふと思いついたことをパタパタと書いてます。

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