xUTP Topics: 第三回 xUnit Test Patterns の世界観「テストコードの不吉な臭い」

書いた人:@yujiorama(id:yujiorama)

目次

  • 目的
  • はじめに
  • テストの匂い
  • テストコードの匂い
    • Obscure Test
    • Conditional Test Logic
    • Hard-to-Test Code
    • Test Code Duplication
    • Test Logic in Production
  • 振る舞いの匂い
    • Assertion Roulette
    • Erratic Test
    • Fragile Test
    • Frequent Debugging
    • Manual Intervention
    • Slow Tests
  • プロジェクトの匂い
    • Buggy Tests
    • Developers Not Writing Tests
    • High Test Maintenance Cost
    • Production Bugs
  • さいごに

目的

この連載記事の目的は次のような感じです。

  • xUTP読書会で得られた知見を整理する
  • xUnit Test Patterns に書かれている内容を分かりやすい形で広める

英語の壁も、難しさの壁も、読書会の有志による努力によって取り払われました。

軽い気持ちで、xUnit Test Patterns に挑戦してみましょう。

はじめに

xUnit Test Patterns(以下 xUTP) では、ソフトウェア開発全体におけるテストの在り方や考え方について説明されています。

今回は、開発を進めていく中でよく遭遇するアンチパターンである、「テストの匂い」についてご紹介します。

xUTP 読書会 Wiki で該当するページは次のとおりです。

テストの匂い

「匂い」とは、問題が発生する兆候のことです(Part1 Chapter 2 Test Smells - What's a Test Smells?)。

"リファクタリング"では、「コードの不吉な匂い」としてプロダクションコードで見つかる問題に注目しています。 xUTP の著者は、テストコードにおいても同様の問題があるのではないかと考えていたそうです。

その後、XP2001 で"Refactoring Test Code"というテストコードに固有の問題 (テストコードの不吉な匂い) や解決方法 (テストのリファクタリング) についての発表があったようです。 (ちなみに、著者は同じセッションで"Increasing the Effecctiveness of Automated Testing"というテストの効率化についての発表をしていました。)

xUTP では、これらの知見を元に整理された「テストコードの不吉な匂い」を紹介しています。

テストコードの匂いは、リファクタリングで紹介された「コードの不吉な匂い」を、テストコードに適用したものです(Part1 Chapter 2. Test Smells - The Code Smells)。

振る舞いの匂いは、テストが失敗する兆しや、そうなりやすいテストの特徴について説明したものです(Part1 Chapter 2. Test Smells - The Behavior Smells)。

プロジェクトの匂いは、テストコードの匂い振る舞いの匂いの組み合わせによって、プロジェクトの支えとなるべき自動テストがうまく働かないことの兆しなどを説明したものです(Part1 Chapter2. Test Smells - The Project Smells)。

以下の文章では xUTP の記載を抜粋して紹介していますが、リファクタリングRefactoring Test Code(XP2001)を参照しているものは、その旨を記載するようにしました。

ざっくりと見回しただけですが、原文ではそれぞれの「匂い」について、症状・影響・原因・解決方法を 1 セットとして解説しています。 詳しくは原文か、xUTP 読書会 Wiki の該当ページを参照してみてください。

テストコードの匂い

Obscure Test

テストがどんな振る舞いを検証してるのか理解するのが困難になっている。

ドキュメントとしてのテストにほど遠い状態だし、メンテナンスにかかるコストがとても高くなる。

テストコードの中にバグが隠れやすくなってしまう。

  • 原因
    • Eager Test
      • 根本原因 一つのテストで検証してる機能が多すぎる
    • Mystery Guest
      • フィクスチャや検証ロジックがテストメソッドの外にあるので、テストの読み手が期待する振る舞いなどを理解できない
    • General Fixture
      • 共通フィクスチャを使っているので、そのテストでどれが必要なのか分からない
    • Irrelevant Information
      • フィクスチャにおいてテストしたいことと無関係な情報が多く、テスト対象がどのような影響を与えているのかテストの読み手を迷わせる
    • Hard-Coded Test Data
      • フィクスチャや期待値、テスト対象への引数がハードコードされているので、テスト対象への入力と期待する結果が分かりにくい
    • Indirect Testing
      • テストメソッドが、間接的にテスト対象とやり取りしているため、複雑に見える

Conditional Test Logic

テストの実行結果は普通なのに、テストメソッドの中の制御構造が複雑で、どのパスが実行されてるのか分からない。

テストコード自体が複雑になっており、プロダクションコードにバグがあったとして、それが意図したように検出できるか分からない。

  • 原因
    • Flexible Test
      • テストコード中の実行されるパスによって検証される内容が変わる
    • Conditional Verification Logic
      • if 文を使って期待する結果が得られなかったら fail するなどのコード
      • コレクションを loop を使って繰返し検証し、期待する結果が得られなかったら fail するなどのコード
    • Production Logic in Test
      • 期待値を組み立てるロジックがテストコードに書かれている
    • Complex Teardown
      • テスト後にフィクスチャを解体するコードが複雑でデータリーク (開放漏れ) が起きやすい
    • Multiple Test Conditions
      • テストメソッドの中で複数のデータセットについて、繰返し同じロジックを通している
      • 問題の局所化が難しい

Hard-to-Test Code

自動テストを書くのが困難なコード。

GUI コンポーネントやマルチスレッドのコード、テストコードから参照できないコードや、他のコードへの依存が深すぎてテストコードをコンパイルできないなどの原因がある。

自動テストによる検証ができないので手動で検証するしかないため、コードの改修を確認するなどのプロセスがスケールしない。

  • 原因
    • Highly Coupled Code
      • テスト対象以外のコードについて依存が深すぎる
    • Asynchronous Code
      • テストコードから直接実行できず、なんらかの実行単位 (プロセス、スレッド) から実行しないと動かない
    • Untestable Test Code
      • テストコードが不明瞭だったり、検証ロジックが正しいか分からない

Test Code Duplication

テストコードのあちこちに、同じ内容が書かれている。

テスト対象の呼び出しがコピペされてあちこちに存在するため、テスト対象のインターフェースの改修に追従すべきテストコードが多くなっていく。

  • 原因
    • Cut-and-Paste Code Reuse
      • コピペされたテストコードが大量にあって、それぞれが個別にメンテナンスされている
      • テストでやるべきことよりもどうやってやるかに注意が向きすぎている
    • Reinventing the Wheel
      • 同じテストを書いてしまう、という事故が起きてる
      • 開発者に、他の人が書いたテストを利用するよりも自分で書いてしまう、という傾向がある

Test Logic in Production

プロダクションコードに、テスト時にしか実行されないロジック (内部状態にアクセスする、など) が含まれている。

バグの元になるのであまりお勧めしない。

商用環境を想定した設計になっていないコードが何らかの間違いで実行されてしまうと、深刻な問題を引き起こすことになる。

  • 原因
    • Test Hook
      • プロダクションコードとして動くよう設計されていないコードや、 プロダクション環境において適切に動作するよう検証されていないコードが、 誤って実行され重大な問題を生み出す可能性がある。
    • For Tests Only
      • プロダクションコードにテストのためにだけ使われる箇所がコードを複雑にしている
    • Test Dependency in Production
      • プロダクションコードがテストコードに依存しているため、単独でビルドできない
    • Equality Pollution
      • テストを通すためだけの equals メソッド (等値性の判定処理) が実装されている

振る舞いの匂い

Assertion Roulette

テスト失敗時にどの assertion が失敗したのかよく分からないので、開発者がテストを再現できなくてバグの修正に時間がかかってしまう。

  • 原因
    • Eager Test
      • 根本原因 一つのテストで検証してる機能が多すぎる
      • 一つのテストが検証してる機能が、フィクスチャの準備や assertion などばらばらに呼ばれている
      • テストフレームワークを改造して、assertion が失敗してもその行以降を実行させようとしてる
      • 根本原因 多くの手順が必要な顧客テスト (Customer Test)を xUnit で行おうとしてる
    • Missing Assertion Message
      • 根本原因 同じ Assertion Method が複数回使われている
      • 根本原因 assertion にメッセージが無い

Erratic Test

テストが成功したり失敗したりする。

急いでるからといってテストスイートから外して後で戻し忘れてしまったり、他のテストが失敗したせいで気付かれなかったりする。

原因はいろいろあるので根が深い。

  • 原因
    • Interacting Tests
      • テストが何らかの形で他のテストや環境に依存している
      • テストを単独で実行した場合は成功するが、複数のテストと一緒に実行すると失敗する
      • テストのインスタンスより寿命が長いデータベースや static 変数を使っていることが原因になっていることが多い
      • テスト自体は正しい (false-positive、偽陽性)
    • Interacting Test Suites
      • "Interacting Tests" のテストスイート版
    • Lonley Test
      • "Interacting Tests" の逆
      • テストスイートの一部として実行した場合は成功するが単独で実行すると失敗する
    • Resource Leakage
      • テストを実行するにつれて遅くなり、失敗しだすようになる
      • リソースの開放に失敗している
    • Resource Optimism
      • ある環境で成功するテストが他の環境では失敗する
      • テストの合否が、テストが成功する環境にだけあるリソースに依存している
    • Unrepeatable Test
      • テストの合否が、複数のテストを実行したときの実行順によって変わる
    • Test Run War
      • 一人でテストを実行した場合は成功するが、複数人で実行すると失敗する
      • 「最後の変更を疑え」ルールが機能しなくなる
    • Nondeterministic Test
      • テストの合否が環境にも同時に実行することにも関係無く、ランダム
      • テスト対象のアルゴリズムへの入力に、ランダムな値を使っていると発生する

Fragile Test

修正した箇所と無関係なはずのテストが失敗しはじめる。

時にはプロダクションコードを修正してないのに発生することもある。

  • 原因
    • Indirect Testing
      • テストコードから、他のオブジェクトを介して間接的にテスト対象のオブジェクトにアクセスしている
    • Eager Test
      • 一つのテストで検証してる機能が多すぎる
    • Interface Sensivity
      • インターフェースを修正したらテストコードがコンパイルエラー
      • インターフェースを修正したらテスト実行時に型非互換ランタイムエラー
    • Behavior Sensivity
      • テスト対象のプロダクトコードを修正したら無関係なテストが失敗する
      • 特にフィクスチャの準備、事後状態の検証、フィクスチャの解体で起きる場合が問題
    • Data Sensivity
      • テストデータを変更するとテストが失敗する
    • Context Sensivity
      • テストコード、テストデータは修正してないのに、テストが失敗し始める
      • 時刻や日付、他システムからの入力などに依存している
    • Overspecified Software
      • テストコードで、テスト対象の振る舞いを過剰に検証してる
      • 実装依存
    • Sensitive Equality
      • オブジェクトの等値性の比較が、文字列化した文字列比較になっている
    • Fragile Fixture
      • フィクスチャの準備をするコードを修正したら、無関係なテストが失敗する

Frequent Debugging

テストが失敗してもメッセージから特定できないため、デバッガを使うことがしょっちゅうある。

ユニットテストの粒度が大きすぎたり、テスト自体が不足しているような場合もある。

  • 原因
    • Production Bugs
      • "Infrequetly Run Tests" 開発者がテストを稀にしか動かしていない
      • "Untested Requirements" 皆が気付いているのにテストされてない機能性がある

Manual Intervention

テストのたびに何らかの手作業が必要になっていて、めったにテストが実行されない。

テストの自働化ができない。

  • 原因
    • Manual Fixture Setup
      • フィクスチャの準備が手作業
    • Manual Result Verification
      • テスト結果の検証が手作業
      • テスト結果が正しくなくてもテスト自体は正常終了してしまう
    • Manual Event Injection
      • テストの実行中に手動でなんらかの介入をする
      • ネットワークケーブルを抜いたりボタンをクリックしたり

Slow Tests

テストの実行に時間がかかる。

  • 原因
    • Slow Component Usage
      • 遅いコンポーネントを使っている
      • データベースとのやりとり、など
    • General Fixture
      • 巨大になってしまった汎用フィクスチャ
      • テストケースごとにフィクスチャを準備するため、テストの数だけ所要時間が増えていく
    • Asynchronous Test
      • テストやテスト対象に非同期処理を行っているものがあり、その中で待ち状態になっている
    • Too Many Tests
      • テストの数が多すぎる

プロジェクトの匂い

Buggy Tests

自動化テストがうまくいってないことを示す兆し。

テスト対象のコードは正しいとしか思えないのにテストが失敗する。 (false positive、偽陽性)

テストコードにバグがあって、プロダクションコードのバグを見逃してしまう。 (false negative、偽陰性)

  • 原因
    • Fragile Test
      • テストもテスト対象も正しいはずなのに、テストが失敗するようになってしまう
    • Obscure Test
      • 失敗するはずのテストが成功してしまう
    • Hard-to-Test Code
      • 自動テスト用のテストスイートがないレガシーコード

Developers Not Writing Tests

開発者がテストを書いてくれず、Production Bugs に対して「テストでカバーできなかった」と言い訳する。

「テスト」という負債のため、設計を改善するためにコードを書き換えるのが危険なことになっている。

  • 原因
    • Not Enough Time
      • 開発スケジュールが厳しい
      • 効率良くテストを書くスキルを開発者が持ってないし、学習する時間もない
    • Hard-to-Test Code
      • テストが書きにくい設計になっている
    • Wrong Test Automation Stragety
      • テスト戦略の誤り
      • テスト環境のせいでテストが書きにくくなっている

High Test Maintenance Cost

ちゃんとテストを書いているのに、新機能の開発速度が徐々に落ちていく。

コードを書いている時間の大部分を、既存のテストコードの修正に費している。

開発者が、失敗するようになったテストをテストスイートから削除している。

  • 原因
    • Fragile Test
      • プロダクションコードの軽微な改修でもテストが失敗しはじめる
    • Obscure Test
      • 既存のテストが理解しづらく、テストのデバッグに時間を費している
    • Hard-to-Test Code
      • プロダクションコード (レガシーコード) が十分に安定してしまっているため、テストを書くのが大変

Production Bugs

自動化テストを書いているのに、テストチームによるテスト、顧客によるテスト、商用環境で多くのバグが発見される。

  • 原因
    • Infrequently Run Tests
      • 開発者はテストをあまり実行してないし、日次の結合ビルドのテストも失敗している
    • Lost Test
      • テストが削除されたり無効にされたりしていて、実際に実行されてるテストの数がやけに少ない
    • Missing Unit Test
      • 改修があってもユニットテストが追加されてないので、リファクタリングでどこかの機能を壊してしまう
    • Untested Code
      • テスト対象のコードパスが他コンポーネントに依存しており、それをテストする方法が分からないのでテストが書けない
    • Untested Requirement
      • テスト対象の個々の部品はテストしているが、システムの機能のテストをしてない
    • Neverfail Test
      • assertion が間違っていて絶対に失敗しなくなっている
      • 非同期システムのテストにおいて、テストを実行しているスレッドとは異なるスレッドやプロセスから例外がスローされて見逃している

さいごに

メリハリ無く書き連ねたせいで、「匂い」間の関連性は表現できませんでした。 関連性とは、Code Smells や Behavior Smells を放置すると Project Smells の原因となる、といったことです。

Project Smells が感じられるということは、それだけ根が深い問題なのかもしれません。 自分たちの身の回りを見直す際の観点として利用してみてはいかがでしょうか。

Last modified:2012/06/10 00:24:05
Keyword(s):
References:[xUTP Magazine 0004号] [ぺけま] [SideMenu]