Fluentd開発で学んだtest-unitのイカした使い方5選

# はじめに

こんにちは。
[クリアコード](https://www.clear-code.com/)という会社で自由ソフトウェア（オープンソースソフトウェア）開発に携わっている[daipom](https://github.com/daipom)です。

この1年は[Fluentd](https://github.com/fluent/fluentd)のメンテナーとしてRubyをたくさん使い、Rubyのすごさを色々と体験しました。
この記事では、僕が体験した`test-unit`のイカした使い方を5つ紹介します！

1. テストを実行するときに`TESTOPTS`を活用する
1. [power_assert](https://github.com/ruby/power_assert)を使ってデバッグを容易にする
1. `setup`で`yield`する
1. テストデータは多重代入で直接受け取れる
1. `keep: true`でテストデータをシェアする

# 1. テストを実行するときに TESTOPTS を活用する

Fluentdは`test-unit`を使っており、テストの実行は通常次のように行います。

```console
$ bundle exec rake test
```

そして、`TESTOPTS`を使うことで、テストの実行の仕方をカスタマイズできます。
僕がよく使うパターンは次です。

* `bundle exec rake test TESTOPTS="-t'xxx::yyy' -v"`
  * `xxx::yyy`クラス内のテスト全部を出力詳しめ（`verbose`）で実行
  * `sub_test_case`もこれで指定できます
* `bundle exec rake test TESTOPTS="-t'xxx' -n'yyy'"`
  * `xxx`クラス内の`yyy`テストのみを実行
  * ただし`test 'foo' do ~ end`の書き方の場合、`test: `というプレフィックスが自動でテスト名に追加される点に注意が必要です
    * この場合は`-n'test: foo'`のように指定する必要があります
    * `data`を使っている場合、`[データ名]`のサフィックスを付けることで使用するデータまで指定することができます（省略可能で、省略すれば各データのパターンを全部実行します）
* `bundle exec rake test TESTOPTS="-v --no-show-detail-immediately"`
  * CIはこれ！！
  * 出力詳しめ（`verbose`）、かつ失敗したテストの情報は末尾にまとめて出す！！
  * Fluentdは4000個以上のテストがあるので、これが必須
    * [FluentdのCI実行例](https://github.com/fluent/fluentd/blob/v1.18.0/.github/workflows/test.yml#L44-L45)

指定できるオプションは次のように確認できます。

```console
$ bundle exec ruby {適当なテストファイル} --help
```

# 2. `power_assert`を使ってデバッグを容易にする

テストが失敗したときに、関連する変数の中身がどうなっていたのかを知りたいこと、かなりありますよね。
そういうとき、[power_assert](https://github.com/ruby/power_assert)がとても便利です。

使い方は簡単。次のように、`assert`のブロックの中で満たすべき条件判定を行うだけです。

```rb
assert { logs.any? { |log| log.include?("${chunk_id} is not allowed in this plugin") } }
```
https://github.com/fluent/fluentd/blob/v1.18.0/test/plugin/test_output.rb#L394C7-L394C95

結果が`false`ならばテストが失敗し、このブロックの中で使われた各変数の中身がいい感じに表示されます。

(期待値を変えて無理やり失敗させた場合の出力)
```
Failure: test: #extract_placeholders logs warn message if metadata is passed for ${chunk_id} placeholder(OutputTest::basic output feature):
        assert { logs.any? { |log| log.include?("foo") } }
                 |    |
                 |    false
                 ["2024-12-19 20:39:53 +0900 [warn]: ${chunk_id} is not allowed in this plugin. Pass Chunk instead of metadata in extract_placeholders's 2nd argument\n", "2024-12-19 20:39:53 +0900 [warn]: chunk key placeholder 'chunk_id' not replaced. template:/mypath/${chunk_id}/tail\n"]
```

`logs`にどういうログが入っていたのかを確認できて便利ですね！
ちなみにこれを`power_assert`を使わずに`assert_equal`などでやると、次のようになります。

```rb
assert_equal true, logs.any? { |log| log.include?("foo") }
```

```
<true> expected but was
<false>

diff:
? tru e
? fals 
? ???
```

これではなぜ`false`になったのかが分からず困りますよね。

単純なアサーションであれば`assert_equal`などで十分ですが、今回の例のように変数をフィルターするようなアサーションであれば`power_assert`を使っておくと、捗ること間違いなしです。

# 3. `setup`で`yield`する

各テストの実行前処理をセットできる`setup`(`Test::Unit::TestCase#setup`)メソッドですが、`yield`を使えることをご存知でしょうか？

`yield`を使うと、各テストを`yield`で実行する形となります。
そのため、例えば次のように一時ファイルを使うテストを簡潔かつ安全に書けます。

```rb
def setup
  Tempfile.create('intail_position_file_test') do |file|
    file.binmode
    @file = file
    yield
  end
end
```

https://github.com/fluent/fluentd/blob/v1.18.0/test/plugin/in_tail/test_position_file.rb#L9-L15

`yield`でテストをするので、各テスト内では`@file`で一時ファイルを使用できます。
テストが完了すると、その一時ファイルは破棄されます。

例えば次のようにテストを書けます。

```rb
test '.load' do
  write_data(@file, TEST_CONTENT)
  Fluent::Plugin::TailInput::PositionFile.load(@file, ...)

  @file.seek(0)
  lines = @file.readlines
  assert_equal 2, lines.size
  ...
end
```

簡潔でいいですね！
色々な活用の仕方もできそうです！

ただし、`sub_test_case`でテスト前処理を追加する時によく使う`setup do ~ end`ではこの機能を使えないので、ご注意ください。

# 4. テストデータは多重代入で直接受け取れる

`test-unit`のデータ駆動機能、活用していますか？
テストコードを最小限に保ちつつ、テストパターンを充実させるのに大活躍しますよね。

その際、次のようにテストデータを多重代入で直接受け取ることができます。

```rb
data("oj", [:oj, [Oj.method(:load), Oj::ParseError]])
data("json", [:json, [JSON.method(:load), JSON::ParserError]])
data("yajl", [:yajl, [Yajl.method(:load), Yajl::ParseError]])
def test_return_each_loader((input, expected_return))
  result = @parser.instance.configure_json_parser(input)
  assert_equal expected_return, result
end
```

https://github.com/fluent/fluentd/blob/v1.18.0/test/plugin/test_parser_json.rb#L12-L18

まあ次のようにテストメソッド内の1行目で多重代入すればいいのですが、やりたいことをそのままコードに書けている感じがして嬉しいです。

```rb
data("test1", ["foo", "bar"])
def test_foo
  foo, bar = data
  ...
end
```

引数で多重代入した方が、なんとなくデータ表のリストも見やすくなる感じがします。
何番目が何のデータなのかが、パッと頭に入ってきやすいです。

次のようにブロックを使う書き方でも使えます。

```rb
data("test1", ["foo", "bar"])
test "foo" do |(foo, bar)|
  ...
end
```

# 5. `keep: true`でテストデータをシェアする

4につづきデータ駆動機能の話です。

ある1つのデータに対して複数のテストを定義したい、ということありますよね。
それは`keep: true`で可能です。

```rb
sub_test_case 'without <system> directive' do
  data(
    'without <worker> directive',
    {
      target_worker_ids: [],
      expected: 1
    },
    keep: true
  )
  data(
    'with <worker 0>',
    {
      target_worker_ids: [0],
      expected: 1
    },
    keep: true
  )

  test 'system_config.workers value after configure' do
    assert_system_config_workers_value(data)
  end

  test 'system_config.workers value after configure  with supervisor_mode' do
    stub_supervisor_mode
    assert_system_config_workers_value(data)
  end
end
```

https://github.com/fluent/fluentd/blob/v1.18.0/test/plugin/test_base.rb#L219-L245

この`sub_test_case`内のこの2つのテストは、どちらもこの2つのテストデータを利用します。
1テスト内で無理やり分岐させずに済むので、色々なパターンのテストを簡潔に書くことができます。

# おわりに

この記事では、僕が体験した`test-unit`のイカした使い方を5つ紹介しました。
テストコードはとても大事ですが、その維持管理に悩まされたくはないですよね。
Rubyの便利な機能を駆使して、テストの維持管理が楽に、そして楽しいものになれば嬉しいなあと思います。

最後に、[クリアコード](https://www.clear-code.com/)は[Fluentd](https://github.com/fluent/fluentd)を始めとした様々な自由ソフトウェア（オープンソースソフトウェア）の開発・メンテナンスを行い、日々得られた知見を[ブログ](https://www.clear-code.com/blog/)で紹介しています。
もし興味があればそちらもぜひ見ていただけると嬉しいです。

それでは。