標準入力と標準出力をテストしようとすることは基本的にはありません。標準出力ではなく、Loggerに書き込まれていることを確認することが多いでしょう。
しかし、知っておいて損はありません。今回の記事では、FizzBuzzを使って標準入力と標準出力のテストを行います。
なお、新規性はありませんので、参考先の記事を見てみてください。
環境
- Java
- 15
仕様
- 標準入力に1と入力したとき、標準出力には1が出力されること
- 標準入力に2と入力したとき、標準出力には2が出力されること
- 標準入力に3と入力したとき、標準出力にはFizzが出力されること
- 標準出力に他の値が出力されていないこと
FizzBuzzの仕様
この記事では、FizzBuzzの仕様は以下の通りです。FizzBuzzそのもののロジックの説明はしません。
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
標準入力と標準出力のテスト
標準入力と標準出力の準備
定義として、標準入力はSystem.in
、標準出力はSystem.out
のことをを指します。まずは、データの設定、データの取得ができるようにSystem.in
とSystem.out
を差し替えるクラスを作成します。
差し替える標準入力
InputStream
を継承しているクラスを作る必要があります。今回はStandardInputStream
という名前で作成します。テストしやすいようにデータを設定できるinputln
メソッドを用意します。
public class StandardInputStream extends InputStream { private StringBuilder sb = new StringBuilder(); private String lf = System.getProperty("line.separator"); /** * 文字列を入力する。改行は自動的に行う * * @param str 入力文字列 */ public void inputln(String str) { sb.append(str).append(lf); } @Override public int read() { if (sb.length() == 0) return -1; int result = sb.charAt(0); sb.deleteCharAt(0); return result; } }
差し替える標準出力
PrintStream
を継承しているクラスを作る必要があります。今回はStandardOutputStream
という名前で作成します。標準出力したデータを取得できるreadLine
メソッドを用意します。
public class StandardOutputStream extends PrintStream { private BufferedReader br = new BufferedReader(new StringReader("")); public StandardOutputStream() { super(new ByteArrayOutputStream()); } /** * 1行分の文字列を読み込む * @return 改行を含まない文字。終端の場合はnull */ public String readLine() { String line = ""; try { if ((line = br.readLine()) != null) return line; br = new BufferedReader(new StringReader(out.toString())); ((ByteArrayOutputStream) out).reset(); return br.readLine(); } catch (IOException e) { throw new RuntimeException(e); } } }
テスト対象のプロダクトコード
FizzBuzzの変換クラスと、標準入力から受け取った値をFizzBuzzクラスに渡して標準出力に渡すクラスを作成します。
public class FizzBuzz { public String convert(int input) { var sb = new StringBuilder(); if (input % 3 == 0) sb.append("Fizz"); if (input % 5 == 0) sb.append("Buzz"); if (sb.length() == 0) sb.append(input); return sb.toString(); } } public class FizzBuzzInput { public void main() { FizzBuzz fizzBuzz = new FizzBuzz(); try (Scanner sc = new Scanner(System.in)) { System.out.println(fizzBuzz.convert(sc.nextInt())); System.out.println(fizzBuzz.convert(sc.nextInt())); System.out.println(fizzBuzz.convert(sc.nextInt())); } } }
テストコード
標準入力はSystem.setIn
、標準出力はSystem.setOut
で差し替えることができます。
@BeforeEach
でテストを実行するたびに、先ほど作成したStandardInputStream
、StandardOutputStream
を設定します。なお、System
の値はグローバルなので、そのままに設定していると他のクラスに影響が出てしまいます。@AfterEach
でSystem.setIn
とSystem.setOut
に初期値を設定することを忘れないようにしましょう。
初期値については、Java15でのSystem
クラスで確認しておりますが、最新版で処理を確認しておくと良いでしょう。
private StandardInputStream in = new StandardInputStream(); private StandardOutputStream out = new StandardOutputStream(); @BeforeEach public void before() { System.setIn(in); System.setOut(out); } @AfterEach void tearDown() throws Exception { System.setIn(new BufferedInputStream(new FileInputStream(FileDescriptor.in))); System.setOut(new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.out), 128), true, StandardCharsets.UTF_8)); }
標準入力にデータを設定するinputln
メソッド、標準出力からデータを取得するreadLine
メソッドを用意したので、そこからテストデータのセット、及びデータの検証をしましょう。今回は、標準入力に1, 2, 3を渡し、標準出力に1, 2, Fizzが取得できることを確認します。
FizzBuzzInput target = new FizzBuzzInput(); @Test public void _1and2AndFizzAndNull() { // Given in.inputln("1"); in.inputln("2"); in.inputln("3"); // When target.main(); // Then assertThat(out.readLine()).isEqualTo("1"); assertThat(out.readLine()).isEqualTo("2"); assertThat(out.readLine()).isEqualTo("Fizz"); assertThat(out.readLine()).isNull(); }
厳密なFizzBuzz問題
TDDでの演習を行う場合、変換するロジックだけをテストします。しかし、次の強調した箇所に関しては無視されることが多いです。
**1から100**までの数を**プリント**するプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」と**プリント**し、3と5両方の倍数の場合には「FizzBuzz」と**プリント**すること。
プロダクトコード
このプリントは標準出力と読み替えることができます。厳密に仕様を満たすと次のようなコードになるのではないでしょうか。
public class FizzBuzz { public void execute() { // 1から100まで実行 for (int i = 1; i <= 100; i++) { // プリントする System.out.println(convert(i)); } } // 変換する public String convert(int input) { // 省略 } }
テストコード
このFizzBuzzの厳密な定義で作ったexecute
メソッドのテストは次の観点を含むので、初心者には非常に難しいです。
- 標準出力のテストが難しい
- リターンの値が無いので容易にテストができない
- 本当に1-100まで出力しているか分からない
- 1-100まで順番に出力していない可能性があるかもしれない
しかも、何も考えずにテストしてしまうと、アサーションルーレットになってしまいます。妥協案としては、SoftAssertions
を使ってアサーションルーレットを回避するべきではないでしょうか。
@Test void execute() { target.execute(); int i = 0; SoftAssertions softly = new SoftAssertions(); String actual = out.readLine(); // 標準出力の値と比較 while (actual != null) { softly.assertThat(target.convert(++i)).isEqualTo(actual); actual = out.readLine(); } // 標準出力に書き込まれた回数の比較 softly.assertThat(100).isEqualTo(i); softly.assertAll(); }
備考
ポートアンドアダプターで考えると、標準入力、標準出力の機能はコアではなく、アダプターの機能です。ポートアンドアダプターについては、類似記事を参考にしてみてください。
ソースコード
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/main/java/kirimaru/biz/domain/FizzBuzz.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/main/java/kirimaru/biz/domain/FizzBuzzInput.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/test/java/kirimaru/biz/domain/FizzBuzzTests.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/test/java/kirimaru/biz/domain/FizzBuzzInputTests.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/main/java/kirimaru/biz/domain/StandardInputStream.java
- https://github.com/hirotoKirimaru/cucumber-sample/blob/9feb0f022236d9bbc57bd9c6f80d91f26cf89739/kirimaru-core/src/main/java/kirimaru/biz/domain/StandardOutputStream.java
終わりに
TDDでのFizzBuzzはTDDのリズムを学んでもらうもので、標準出力にこだわるべきではありません。しかし、TDDを理解するためにあえて仕様を無視をするのと、仕様を無視する癖をつけてしまうのは違います。
基本的に仕様を無視して良い理由などありませんので、伝えたいことがぼやけない様にFizzBuzzのお題を提供する側も注意しないといけませんね。
あと、私はやっていないのですが、AtCoder等の競技プログラミングでは標準入力でデータを受け取るようなので、ローカルでテストができるように標準入力の差し替えを素振りしておくといいかもしれません。
この記事お役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。
参考記事
- http://www.aoky.net/articles/jeff_atwood/why_cant_programmers_program.htm
- https://qiita.com/aky100200/items/f4f7d6279524774610fc
類似記事
- もっといいテストが書きたい(AssertEqualsとAssertThatの検証) - きり丸の技術日記
- https://nainaistar.hatenablog.com/entry/2021/07/19/120000