1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RubyAdvent Calendar 2024

Day 19

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

Last updated at Posted at 2024-12-19

はじめに

こんにちは。
クリアコードという会社で自由ソフトウェア(オープンソースソフトウェア)開発に携わっているdaipomです。

この1年はFluentdのメンテナーとしてRubyをたくさん使い、Rubyのすごさを色々と体験しました。
この記事では、僕が体験したtest-unitのイカした使い方を5つ紹介します!

  1. テストを実行するときにTESTOPTSを活用する
  2. power_assertを使ってデバッグを容易にする
  3. setupyieldする
  4. テストデータは多重代入で直接受け取れる
  5. keep: trueでテストデータをシェアする

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

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

$ 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テストのみを実行
  • bundle exec rake test TESTOPTS="-v --no-show-detail-immediately"
    • CIはこれ!!
    • 出力詳しめ(verbose)、かつ失敗したテストの情報は末尾にまとめて出す!!
    • Fluentdは4000個以上のテストがあるので、これが必須

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

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

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

テストが失敗したときに、関連する変数の中身がどうなっていたのかを知りたいこと、かなりありますよね。
そういうとき、power_assertがとても便利です。

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

assert { logs.any? { |log| log.include?("${chunk_id} is not allowed in this plugin") } }

結果が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などでやると、次のようになります。

assert_equal true, logs.any? { |log| log.include?("foo") }
<true> expected but was
<false>

diff:
? tru e
? fals 
? ???

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

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

3. setupyieldする

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

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

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

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

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

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のデータ駆動機能、活用していますか?
テストコードを最小限に保ちつつ、テストパターンを充実させるのに大活躍しますよね。

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

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

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

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

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

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

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

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

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

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

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

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

おわりに

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

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

それでは。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?