Quantcast
Channel: hachi8833の記事一覧|TechRacho by BPS株式会社
Viewing all articles
Browse latest Browse all 1759

テストを不安定にする5つの残念な書き方(翻訳)

$
0
0

こんにちは、hachi8833です。今回はテストを正しく書く方法を解説する記事の翻訳をお送りします。

概要

原著者の許諾を得て翻訳・公開いたします。

テストを不安定にする5つの残念な書き方(翻訳)

不安定なテストは、毎日の苦労を台無しにしてくれる技術上の負債の一部となります。テストが不安定だとCIが赤信号になってしまい、それだけのために新しいコードのリリースを中断してビルドをやりなおすはめになります。実際のコードはどこもおかしくないのに、どこかがおかしいのではないかという疑念が湧くと、ストレスの元になります。

数百人の開発者と5万件のテスト項目があるような大規模案件では、不安定なテストが混入する可能性がさらに高まります。

本記事で扱うデモの中には、テストの実行順序に関連するものもありますが、そうでないものもあります。テストの実行順序とは何か、それがテストにどう関連するのか。それを確認する一番の方法は、テストの実行順序をランダムにしてみることでしょう。そうすれば、あるテストが他のテストに紐付けられていないことと、テストが実行順序に依存していないことがわかります。

本記事ではMiniTestを用いますが、どの問題もフレームワークに依存しないものばかりなので、MiniTestであるかRSpecであるかを問わず、一般的に応用できます。

1. ランダムなファクトリー

# emailフィールドにはunique制約がかかっているとする
10.times do
  Customer.create!(email: Faker::Internet.safe_email)
end

どこに問題があるかおわかりでしょうか。

このテストはほとんどの場合パスしますが、ごくたまに、既に使ったメールアドレスがFakerからもう一度返されることがあります。そしてunique制約エラーに引っかかってテストがクラッシュするというわけです。

正しい書き方はこうです。

10.times do |n|
  Customer.create!(email: Faker::Internet.safe_email(n.to_s))
end

Fakerに渡す引数を、(デフォルトの)ランダムなメールではなく、n番目のメールを指定するのがポイントです。

2. データベースのレコード順

assert_equal([1, 2, 3], @products.pluck(:quantity))

このテストもほぼすべてのケースでパスしますが、ORDERなしのSELECTクエリではレコードの順序が保証されません。こうしたランダムな要素が原因のエラーを回避するには、次のように明示的に順序を指定します。

assert_equal([1, 2, 3], @products.pluck(:quantity).sort)
# または
assert_equal([1, 2, 3], @products.order(:quantity).pluck(:quantity))

3. グローバル環境の汚染

BulkEditor.register(User) do
  attributes(:email, :password)
end
assert_equal [:email, :password], BulkEditor.attributes_for(@user)

私の経験した例では、登録されたモデルのリストをBulkEditorがグローバルな環境に保存していたことがありました。これでテストを実行するとレジストリが汚されてしまい、その後に実行される他のテストが影響されてしまいます。順序依存の一丁上がりです。

解決方法は次のとおりです。

setup to
  BulkEditor.register(User) do
    attributes(:email, :password)
  end
end

teardown do
  BulkEditor.unregister(User)
end

私の経験したグローバル環境汚染からもうひとつご紹介しましょう。

test "something" do
  SomeGem::VERSION = '9999.99.11'
  assert_not @provider.supported?
end

このテストコードの後に実行されるテストでは、SomeGem::VERSIONから返る値がおかしくなってしまいます。さらに、Rubyレベルでのワーニング「warning: already initialized constant SomeGem::VERSION」が表示されます。

解決方法は次のとおりです。

test "something" do
  # 変更された定数値をブロックだけが受け取るようにする
  stub_constant(SomeGem, :VERSION, '9999.99.99') do
    assert_not @provider.supported?
  end
end

4. 時間に依存するテスト

post = publish_delayed_post
assert_equal 1.hour.from_now, post.published_at

このテストは通常であればパスしますが、ごくたまに、postの公開に要する時間が1msecを超え、かつ#published_atの所要時間が1.hour.from_nowを超えることがあります。

こういう場合に便利なのは、#assert_in_deltaという特殊なヘルパーメソッドです。

post = publish_delayed_post
assert_in_delta 1.hour.from_now, post.published_at, 1.second

他にも、Timecopなどのライブラリで時間を止める方法があります。

5. requireに依存するテスト

テスト用のクラスが2種類あり、1つはリモートHTTP呼び出しを許可するがもう1つは許可しないとします。以下のような感じのコードになるでしょう。

# test/unit/remote_api_test.rb
require 'remote_test_helper'

class RemoteServiceTest < ActiveSupport::TestCase
  test "something" do
    # ...
  end
end

# test/unit/simple_test.rb
require 'test_helper'

class SimpleTest < ActiveSupport::TestCase
  test "something" do
    # ...
  end
end

外部へのHTTP呼び出し用に#remote_test_helperを使うテストが大量にあるとします。私見では、こうしたテストは単独であればおそらく完璧に動作します。しかしCIですべてのテストを通しで実行すると、テストの順序によってはリモート呼び出しの後のあらゆるテストで外部呼び出しができてしまう可能性があります😱

requireグローバルであり、グローバルな状態を変更します。このことを肝に銘じておいてください。

よりよい解決方法は、特定のテストのコンテキストのみを変更するマクロを使うことです。

# test/unit/remote_api_test.rb
require 'test_helper'

class RemoteServiceTest < ActiveSupport::TestCase
  allow_remote_calls!

  test "something" do
    # ...
  end
end

# test/unit/simple_test.rb
require 'test_helper'

class SimpleTest < ActiveSupport::TestCase
  test "something" do
    # ...
  end
end

まとめ

実行結果の不安定なテストの修正はたいてい面倒なものであり、それだけでブログ記事が1本書けるほどです。ひとまず、こうした不安定な要素をテストに持ち込まないよう注意することをおすすめします。

不安定なテストについてご興味がおありの場合は、以下の英語記事をご覧ください。

関連記事


Viewing all articles
Browse latest Browse all 1759

Trending Articles