テストがうまくいかない兆候- テストスメル
はじめに
Martin Fowlerがリファクタリングが必要となるような、コードの不吉な兆候を表す言葉としてコードスメル(Code Smell)という概念を「リファクタリング(第2版): 既存のコードを安全に改善する」で普及させました。
不可思議な名前や重複したコードなどのコードスメルは広く知られています。
テストスメル(Test Smell)は、テストコードにも存在する「リファクタリングが必要となる不吉な兆候」を表す概念として提唱されました。
この記事ではテストスメルについての紹介、各プログラミング言語に応じたテストスメルの検知方法、手動テストにおけるテストスメルについて説明します。
テストスメルの歴史
Refactoring Test Code
2001年にArie van DeursenらはRefactoring Test Codeでテストコード固有の悪い兆候について整理しました。
この時は以下の11個がテストスメルとして紹介されています。
- Mystery Guest
- テストが外部リソースを利用しており、そのテストについて自己完結的ではなくなる
- Resource Optimism
- 外部リソース(特定のディレクトリやデータベーステーブルなど)が楽観的な前提に基づいており、ある環境では動作するが別の環境では動作しない状態
- Test Run War
- 一人のみがテストを実行しているときは合格するが、複数の人間がテストを実行する場合にテストが失敗する状態
- General Fixture
- あまりに多くのテストのための汎用的なフィクスチャ[1]により、読みにくくなったり、特定のテストでは不要な処理が多くなり遅くなったりするケース
- Eager Test
- 1 つのテストメソッドでテスト対象の複数のメソッドを検査することで、読みにくくなったりするケース
- Lazy Test
- Eager Testとは逆で同じテスト対象のメソッドを同じ前提状態で、複数のテストで検証しているケース。複数のテストを合わせて初めて意味が通る状態
- Assertion Roulette
- 多数のアサーションが1つのテストメソッドに並び、どこが失敗したかがわからない状態
- Indirect Testing
- テストクラスはそれに対応した本番のクラスをテストすべきであるが、テストクラスに別のオブジェクトのテストが含まれる状態
- For Testers Only
- 本番クラスにテストメソッドからしか使われないメソッドが含まれる状態
- Sensitive Equality
- toStringによる等価チェックは容易であるが、カンマや引用符、スペースなどの細部の違いで壊れやすくなる
- Test Code Duplication
- テストコード中のコードの重複
なお、原文には、その兆候をどうリファクタリングするかについても記載がありますが、今回はテストスメルの抽出のみとしています。
xUnit Test Patterns
2007年に発表されたxUnit Test Patterns: Refactoring Test CodeではRefactoring Test Codeで提唱されたテストスメルをさらに発展させました。
詳細は書籍を読んだほうが望ましいですが、以下のページでも、その内容が確認できます。
xUnit Test Patternsではコード上の悪い兆候だけでなく、テストコードの振る舞いやテストコードに関連するプロジェクトの状況についての悪い兆候についても言及しています。
- Code Smells
-
Obscure Test
- わかりにくいテスト、不明瞭なテスト
- 以下が原因となる
- Eager Test * Refactoring Test Code参照
- Mystery Guest * Refactoring Test Code参照
- General Fixture * Refactoring Test Code参照
- Irrelevant Information
- 不要な細部が大量に記載されておりテスト対象の振る舞いに本当に影響する事柄からテスト読者の注意をそらしてしまう状態
- Hard-Coded Test Data
- データの値がテストメソッド中にハードコードされており入出力の因果関係がわかりにくくなっている
- Indirect Testing *Refactoring Test Code参照
-
Conditional Test Logic
- テストの中に、実行されるかどうか分からないコードが含まれている状態
- 以下が原因となる
- Flexible Test
- テストコードが、実行する時や場所によって異なる機能を検証している状態
- Conditional Verification Logic
- テスト対象が正しいオブジェクトを返さない場合のアサーションの実行の防止や、コレクションを返したときにループで検証している状態
- Production Logic in Test
- 本番コードの分岐をコピーしてテストコードに利用している状態
- Complex Teardown
- 複雑すぎる終了処理がある状態
- Multiple Test Conditions
- 同一のテストロジックを多数の入力組に適用し、それぞれに対応する期待結果とループで回して比較する。
- Flexible Test
-
Hard-to-Test Code
- 自動テストが書きにくいコード
- 以下が原因となる
- Highly Coupled Code
- 他のコードに強く結合しているクラスがあるコード
- Asynchronous Code
- マルチスレッド/マルチプロセスなどの非同期のコード
- Untestable Test Code
- テストコードそのものが条件文がおおかったり可読性が悪くてテスト不能に近い状態
- Highly Coupled Code
-
Test Code Duplication
- 同じテストコードが何度も繰り返されている状態
- 以下が原因となる
- Reinventing the Wheel
- テストに使用する共通処理などを把握せずに同じ処理を書く状態
- Reinventing the Wheel
-
Test Logic in Production
- 本番にデプロイされるコードの中に、テスト中にのみ実行されるべきロジックが含まれている。
- 以下が原因となる
- Test Hook
- テスト対象の条件分岐で「本来のコード」か「テスト専用ロジック」のどちらを実行するかを決めている
- For Tests Only
- テストでのみ使う目的だけの追加メソッドがテスト対象のロジックに混在している
- Test Dependency in Production
- 本番コードだけをビルドできない。本番コードのコンパイルのためにテストコードをビルドへ含める必要がある。あるいは、テスト用実行ファイルが存在しないと本番コードを実行できないケース
- Equality Pollution
- テストの検証につかうためだけの等価性をテスト対象クラスのequalsに実装する
- Test Hook
-
Obscure Test
- Behavior Smells
-
Assertion Roulette
- 同じテストメソッド内に複数のアサーションがあるとき、どれが失敗の原因か判別しづらい
- 以下が原因となる
- Eager Test * Refactoring Test Code参照
- Missing Assertion Message
- assertメソッドのメッセージが欠落しているケース
-
Erratic Test
- 同じテストが失敗したり成功したりする状態。近年はFlaky Testと呼ばれる。
- 以下が原因となる
- Interacting Tests
- テストが何らかの形で他のテストに依存しているケース
- Interacting Test Suites
- Interacting Testsの一形態。単独スイートでは通るが、 複数スイートを同時に動かすと失敗する状態
- Lonely Test
- Interacting Testsの一形態。スイートの一部としてなら動くが、単独では動かない状態
- Resource Leakage
- テストがメモリなどの有限資源を消費してテストの実行がだんだん遅くなり失敗する。
- Resource Optimism
- 外部リソースに依存するテストが、実行する時や場所によって非決定的な結果となる
- Unrepeatable Test
- 初回実行と2回目以降でふるまいが異なる。実質的に、テスト実行をまたいで自分自身と相互作用しているケース
- Test Run War * Refactoring Test Code参照
- Nondeterministic Test
- 現在時刻や乱数によって左右される非決定的なテスト
- Interacting Tests
-
Fragile Test
- テスト対象外のコード修正で、テストが失敗するようになる状態
- 以下が原因となる
- Interface Sensitivity
- テストが利用しているテスト対象のインターフェイスの一部が変更されたために、テストのコンパイルや実行が失敗することを指します。
- Behavior Sensitivity
- テスト対象の変更によって以前は通っていたテストが失敗し始める。事前状態の検証、事後状態の検証、tearDownで使用しているテスト対象の機能が変更された結果、テストが失敗しはじめる状態
- Data Sensitivity
- テストデータベースの内容などのテストをするために使用しているデータが変わった場合にテストが失敗する状態
- Context Sensitivity
- テストが実行される環境の状態や振る舞いが何らかの形で変化したことで、テストが失敗する状態
- Overspecified Software
- テストが、ソフトウェアの構造や振る舞いの仕方について検証しすぎている状態
- Sensitive Equality * Refactoring Test Code参照
- Fragile Fixture
- あるテストのために共通のフィクスチャ[1:1]を直したら別のテストが失敗するようになるような状態
- Interface Sensitivity
-
Frequent Debugging
- テスト失敗の原因特定に手動デバッグが必要になる状態
- 頻繁にテストを実行していないため、どこにバグがあるかを推測できないケースや、Assertion Messageの不足、クラス内部のロジックエラーを指摘してくれるような、詳細な単体テストの不足などの原因が考えられる。
-
Manual Intervention
- テストを実行するたびに、人が何らかの手作業を行う必要がある。
- 以下のような原因がある
- Manual Fixture Setup
- 自動テストを実行する前に、誰かがテスト環境を手作業で準備しなければならないケース
- Manual Result Verification
- テストは実行できるが、正しい結果を返しているかを人が検証しなければならないケース
- Manual Event Injection
- テスト実行の途中で人が介入し、何らかの手作業を行わないとテストを続行できないケース。たとえば、ネットワークケーブルを抜くとか、 GUI上のボタンを押す必要があるとか。
- Manual Fixture Setup
-
Slow Tests
- テストの実行に時間がかかりすぎる状態
- 以下の原因がある
- Slow Component Usage
- テスト対象のシステムに遅いコンポーネント(DBやネットワーク通信)がある。
- モックなどに切り替えて改善するケースがある。
- General Fixture
- 各テストが同じ過剰設計のフィクスチャを毎回構築するため、一貫して遅い
- Asynchronous Test
- マルチスレッドやプロセスに関わる非同期なコードのテストで待機が必要な状態
- Too Many Tests
- 実行するテストが多すぎる
- スイートを適切に分割して実行するテストを減らす。(全部テストすることは不可欠だが、コミット前にそれが必要とは限らない)
- Slow Component Usage
-
Assertion Roulette
- Project Smells
-
Buggy Tests
- 自動テストの中にテスト自体のバグが頻繁に見つかる
- Buggy Tests には多くの原因があり、その大半はCode SmellsやBehavior Smellsとして現れる
- Fragile Test
- 壊れやすいテストは偽陽性のテストの引き金となる
- Obscure Test
- 分かりにくいテストは失敗すべきなのに通ってしまうテストを生みやすい。テストの読者に焦点を当てるリファクタリングが必要となる
- Hard-to-Test Code(テストしにくいコード)
- Fragile Test
-
Developers Not Writing Tests
- 開発者が自動テストを書いていない状態
- 以下の原因が考えられる
- Not Enough Time(時間不足)
- Hard-to-Test Code(テストしにくいコード)
- Wrong Test Automation Strategy(誤ったテスト自動化戦略)
-
High Test Maintenance Cost
- 既存のテストの保守に過度の労力が費やされている状態
- Fragile Test/Obscure Test/Hard-to-Test Codeが原因となる。
-
Production Bugs
- 正式テストや本番環境で、あまりに多くのバグが見つかる。自動テストをすり抜ける
- 以下の原因が考えられる
- Infrequently Run Tests(テストの実行頻度が低い)
- 開発者があまりテストを走らせていない状態。SlowTestやManual Interventionなど、実行を妨げる根本原因がある
- Lost Test(失われたテスト)
- テストスイートで実行されるテスト数が減少してしまうケース。たとえば、Ignoreの付与やコメントアウト、ファイル名の変更でテスト対象から外れることが考えられる
- Missing Unit Test(不足している単体テスト)
- 単体テストはすべて通るのに、顧客テストが失敗するような状態。
- Untested Requirement(未テストの要件)
- テストをしていない機能があるはずなのに、テストが合格する状態
- Neverfail Test(決して失敗しないテスト)
- 機能が動いていないのに必ず成功するテストがある状態。ミューテーションテストなどで検知できる
- Infrequently Run Tests(テストの実行頻度が低い)
-
Buggy Tests
なお、原文には、その兆候をどうリファクタリングするかについても記載がありますが、今回はテストスメルの抽出のみとしています。
その後のテストスメル
その後、テストスメルは正式な文献・灰色文献の双方で幅広く議論され、新しい種別も増えました。それにともない、同じような概念を別のテストスメル名で表現するなどの混乱が発生しました。
2018年、GarousiらはSmells in software test code: A survey of knowledge in industry and academiaで学術 46 件と灰色文献 120 件の計 166 件を対象に テストスメルを体系分類を行いました。
近年では、EASY Lab により類似のテストスメル・カタログが公開されています。
テストスメルの検知
テストスメルのガイドライン化とともに、テストスメルの検知についてのツールもいくつか開発されました。
2021年にWajdi AljedaaniらはTest Smell Detection Tools: A Systematic Mapping Studyでテストスメル検出ツールの系統的マッピングを実施しました。
この研究ではJava、Scala、Smalltalk、C++ のテストスイートにおけるテストスメルを検出するツールが見出され、その大部分がJavaであったことが確認できます。
後年になるとPythonやJavaScriptのテストスメルに焦点をあてたものもいくつか登場します。
Javaのテストスメルを検出
ここではTest Smell Detection Tools: A Systematic Mapping Studyで紹介されたツールのうち、現在ダウンロードが可能なJavaのテストコードを対象としたテストスメル検知ツールについてまとめます。
-
TeCReVis/TeReDetect
- 時期: 2009, 2010
- Eclipse pluginとして提供されているカバレッジツールで、冗長なテストを検出するツールを発表しました。
- 検知対象
- Test Redundancy(TR): テストを削除してもテストスイートの有効性に影響を与えない場合に発生する
- 参照
-
TestHound
- 時期: 2013
- テストフィクスチャに関連するテストスメルの検出に焦点をあてたデスクトップアプリケーション
- 検知対象
- Dead Field(DF): クラスに、どのテストメソッドからも一度も使用されないフィールドがある状態。
- General Fixture(GF)
- Lack of Cohesion of Methods(LCM): 同じテストクラスに入っているテスト群の関心事がバラバラで、共通フィクスチャや目的を共有していない状態。
- Obscure In-line Setup(OISS): テストメソッド内でセットアップ処理が多すぎるテスト。
- Test Pollution(TM): 共有リソースの読み書きといった依存関係を導入するテスト。
- Vague Header Setup (VHS): クラスのヘッダで初期化されているが、コード中で明示的に定義されていないフィールド。
- 参照
-
OraclePolish
- 時期: 2014
- テスト実行中に入力由来のテストスメルについての検出を行うコマンドラインツール
- 検知対象
- Brittle Assertion: テストによって制御されていない入力から導出された値を検査するアサーション
- Unused Inputs: テストによって制御されているにもかかわらずアサーションによって検査されない入力
- 参照
-
PraDeT
- 時期: 2018
- テスト順番の依存によって発生するFlaky Testの検知を行うためのコマンドラインツール
- 検知対象
- Dependent Test(DepT): 他のテストの成功を前提にのみ実行されるテスト
- 参照
-
TEDD
- 時期: 2019
- JUnitで書かれたWebテスト間のテスト依存を検出するツール
- 検知対象
- Dependent Test(DepT): 他のテストの成功を前提にのみ実行されるテスト
- 参照
-
tsDetect
- 時期: 2019
- 新たに導入された11種類のテストスメルを導入した計19種類のテストスメルを検知するツール.デモ動画.
- 検知対象
- Assertion Roulette(AR):説明メッセージ(アサーションメソッドのメッセージ引数)なしで、複数のアサーション文を含むテストメソッド。
- Conditional Test Logic(CTL):if/switch/条件演算子/for/foreach/while などの制御文を1つ以上含むテストメソッド。
- Constructor Initialization(CI):コンストラクタ宣言を含むテストクラス。
- Default Test(DT):テストクラス名が ExampleUnitTest または ExampleInstrumentedTest のもの。
- Duplicate Assert(DA):同一の引数(パラメータ)を持つアサーション文を複数含むテストメソッド。
- Eager Test(ET):複数のプロダクションメソッドに対する複数回の呼び出しを含むテストメソッド。
- Empty Test(EmT):実行可能な文を1つも含まないテストメソッド。
- Exception Handling(EH):throw 文または catch 節を含むテストメソッド。
- General Fixture(GF):setUp(などの初期化)で生成した全フィールドが、同じテストクラス内のすべてのテストメソッドで利用されていない状態。
- Ignored Test(IgT):@Ignore アノテーションを含むテストメソッドまたはテストクラス。
- Lazy Test(LT):複数のテストメソッドが同じプロダクションメソッドを呼び出している状態。
- Magic Number Test(MNT):アサーションメソッドの引数に数値リテラルが含まれている。
- Mystery Guest(MG):ファイルやデータベースのクラスのオブジェクトインスタンスをテストメソッド内に含む。
- Redundant Print(RP):System クラスの print/println/printf/write を呼び出すテストメソッド。
- Redundant Assertion(RA):アサーション文の期待値引数と実際値引数が同一であるテストメソッド。
- Resource Optimism(RO):File インスタンスを exists()/isFile()/notExists() を呼ばずに利用しているテストメソッド。
- Sensitive Equality(SE):オブジェクトの toString() を呼び出すテストメソッド。
- Sleepy Test(ST):Thread.sleep() を呼び出すテストメソッド。
- Unknown Test(UT):アサーション文を1つも含まず、かつ @Test(expected) のパラメータも持たないテストメソッド。
- 参照
-
DARTS
- 時期: 2020
- IntelliJ pluginとして動作する。コミットレベルでテストスメルを検知してリファクタリングが可能となる。デモ動画を参照。
- 検知対象
- Eager Test(ET)
- General Fixture(GF)
- Lack of Cohesion of Methods(LCM)
- 参照
-
RTj
- 時期: 2020
- Rotten Green Testsというテストスメルを検知するツール
- 検知対象
- Rotten Green Tests(RT): 合格はするが、実行されないアサーションを含むテスト。
- 参照
-
RAIDE
- 時期: 2020
- Eclipse pluginとして、JUnitのアサーションに関するテストスメルを検知する。検出ルールとしてはtsDetectを利用
- 検知対象
- Assertion Roulette (AR)
- Duplicate Assert (DA)
- 論文
-
JNose Test
- 時期: 2020
- テストスイートの品質を分析するためのツール.動画1/動画2
- 検知対象
- Assertionless(AL): データと機能をアサートするように動作しているが、実際にはアサートしていないテスト。
- Assertion Roulette(AR):説明メッセージ(アサーションメソッドのメッセージ引数)なしで、複数のアサーション文を含むテストメソッド。
- Conditional Test Logic(CTL):if/switch/条件演算子/for/foreach/while などの制御文を1つ以上含むテストメソッド。
- Constructor Initialization(CI):コンストラクタ宣言を含むテストクラス。
- Default Test(DT):テストクラス名が ExampleUnitTest または ExampleInstrumentedTest のもの。
- Duplicate Assert(DA):同一の引数(パラメータ)を持つアサーション文を複数含むテストメソッド。
- Dependent Test(DepT): 他のテストの成功を前提にのみ実行されるテスト
- Eager Test(ET):複数のプロダクションメソッドに対する複数回の呼び出しを含むテストメソッド。
- Empty Test(EmT):実行可能な文を1つも含まないテストメソッド。
- Exception Handling(EH):throw 文または catch 節を含むテストメソッド。
- General Fixture(GF):setUp(などの初期化)で生成した全フィールドが、同じテストクラス内のすべてのテストメソッドで利用されていない状態。
- Lazy Test(LT):複数のテストメソッドが同じプロダクションメソッドを呼び出している状態。
- Magic Number Test(MNT):アサーションメソッドの引数に数値リテラルが含まれている。
- Mystery Guest(MG):ファイルやデータベースのクラスのオブジェクトインスタンスをテストメソッド内に含む。
- Redundant Print(RP):System クラスの print/println/printf/write を呼び出すテストメソッド。
- Redundant Assertion(RA):アサーション文の期待値引数と実際値引数が同一であるテストメソッド。
- Resource Optimism(RO):File インスタンスを exists()/isFile()/notExists() を呼ばずに利用しているテストメソッド。
- Sensitive Equality(SE):オブジェクトの toString() を呼び出すテストメソッド。
- Sleepy Test(ST):Thread.sleep() を呼び出すテストメソッド。
- Unknown Test(UT):アサーション文を1つも含まず、かつ @Test(expected) のパラメータも持たないテストメソッド。
- Verbose Test: 過度に長く冗長なテスト
- 論文:
この中のツールを現在使用するのであれば、更新状況とテストスメルの網羅状況からして、JNose Testから始めるのが望ましいと思われます。
Pythonのテストスメルを検出
近年、Pythonを対象にしたテストスメルの検出ツールも少数ではあるが増えてきています。
-
PyNose
- 時期: 2021
- PyCharmのプラグインとしてテストコード中のテストスメルを検知する
- 検知対象
- Assertion Roulette: テストケースに説明なしで複数のアサーション文が含まれている。(規定では無効)
- Conditional Test Logic: 制御文(if、for、whileなど)がテストスイートに含まれている。(規定では無効)
- Constructor Initialization: テストスイートにコンストラクタ宣言(init メソッド)が含まれている。
- Default Test: テストスイートに MyTestCase という名前が使われている
- Duplicate Assert: 同一のパラメータを持つアサーション文が複数含まれている。
- Empty Test: テストケースに実行可能な文が含まれていない
- Exception Handling: try/except 文または raise 文がテストメソッドに含まれている。
- Lack of Cohesion of Test Cases: テストスイート内のテストケースがペアワイズコサイン類似度メトリックに従って凝集していない。(デフォルト無効)
- Magic Number Test: 数値リテラルを引数にとるアサーション文が含まれている。(デフォルト無効)
- Obscure In-Line Setup: 10個以上のローカル変数宣言が含まれているテストケース(デフォルト無効)
- Redundant Assertion: 結果が決して変わらないアサーション(例:assert 1 == 1)が含まれている。
- Redundant Print:
print()関数が呼び出されているテストメソッド。(デフォルト無効) - Sleepy Test:
time.sleep()が呼び出されているテストメソッド。 (デフォルト無効) - Suboptimal Assert: 最適でないアサーションが含まれている。[2]
- Test Maverick: 1つのテストケースがセットアップメソッドを使用せずに個別に実行される。(デフォルト無効)
- 参照
-
Pytest-Smell
- 時期: 2022
- CLIツールとしてテストスメルを検知するツール.使用例
- 検知対象
- Assertion Roulette(AR)
- Conditional Test Logic(CTL)
- Duplicate Assert(DA)
- Eager Test(ET)
- Exception Handling(EH)
- Ignored Test(IgT)
- Magic Number Test(MNT)
- Redundant Print(RP)
- Sleepy Test(ST)
- Unknown Test(UT)
- 参照
-
TEMPY
- 時期: 2022
- Pythonのテストスメルを検出するデスクトップアプリケーション.デモ動画
- 検知対象
- Conditional Test Logic:テストメソッドにおける条件構造および反復ループ。
- Exception Handling:テストメソッドにおける例外処理。
- Non-Functional Statement:テストメソッド内の空スコープ。
- Programming Paradigms Blend:同一テストファイル内でのオブジェクト思考的な書き方と手続き言語的な書き方の混在。[3]
- Redundant Print:テストメソッドでの出力呼び出し。
- Sleepy Test:一時停止を強制するテストメソッド。
- Undefined Test:フレームワークのアノテーションがなく、アサーションのみを持つメソッド。
- Unknown Test:アサーションのないテストメソッド。
- Verbose Test:長大なテストメソッド。
- Verifying in Setup Method:テストクラスのセットアップメソッド内でのアサーションメソッド。
- 参照
実際にPythonでテストスメルを検知する場合になにを使用すべきかは非常に難しい問題になります。
PyNoseは特定のIDEのプラグインに依存しているため、無条件に採用できるものではありません。
TEMPYはCIに組み込める作りではないので使用が難しいでしょう。
Pytest-Smellは構文解析に問題があります。テストメソッドの抽出を単純な文字列操作のみで行っているため、うまく動作しないケースが多々あります。具体的にはクラスを使用してテストを書いてあると動作しません。
Pythonでテストスメルを検知する場合は後述の複数言語対応したツールを検討するか、自分で作る必要があります。
一番お手軽に作る方法としてはTEMPYの解析処理だけ流用して、自前でCIに組み込めるツールを作ることかと思います。
# カレントディレクトリにTEMPYがgit clone済みとする
import sys
sys.path.append('./TEMPY/assets')
from python_parser import PythonParser
p = PythonParser("tests/test_cal.py")
items = p.start()
print('===============')
for item in items:
print(item.method_name, item.lines, item.test_smell_type)
JavaScriptのテストスメルを検出
近年、JavaScriptを対象にしたテストスメルの検出ツールも少数ではあるが増えてきています。
-
Steel
- 時期:2021
- JavaScriptを対象にテストスメルを検知するためのCLIツール
- 検知対象
- Assertion Roulette (AR): テストメソッドに文書化されていない複数のアサーションがあり、どれが失敗の原因か特定しにくい。
- Conditional Test Logic (CT): テストメソッドが 1 つ以上の制御文を含んでおり、本来は単純で全ての文を実行すべきところをそうしていない。
- Eager Test (EaT): テストメソッドが本番コードの複数のメソッドを呼び出し、理解と保守を困難にする。
- Lazy Test (LT): 複数のテストメソッドが同じ本番メソッドを呼び出しており、冗長性や相互の不整合を招き得る。
- Duplicate Assert (DA): テストメソッドが同一条件を複数回検査している。異なる値でのテストはしばしば別個のテストケースを要する。
- Magic Number Test (MN): テストメソッド内の assert 文が数値リテラルを引数に含み、その意味が明確でない可能性がある。
- Redundant Print (RP): テストメソッドに print 文が含まれるが、テストケースは人手介入がほとんどない、または全くない形で実行される。
- Empty Test (Emt): 実行可能文を持たないテストメソッドであり、決して実行されない。
- Exception Handling (ExT): テストの合否が、本番/テストメソッドが例外を投げ、それを catch 文で捕捉することに依存している。フレームワーク固有の例外アサーションを用いるべきところでそうしていない。
- Redundant Assertion (RA): テストメソッドに常に真、あるいは常に偽となるアサーションが含まれており、無意味である。
- Unknown Test (UT): テストメソッドにアサーションが含まれず、例外が送出されない限り全実行が合格してしまう。
- Mystery Guest (MG): テストメソッドが外部リソースを使用しており、安定性や性能の問題を引き起こし得る。
- Resource Optimism (RO): テストメソッドが、ファイルのようなリソースの存在を前提としている。
- Ignored Test (IT): テストメソッドが実行から抑制されており、コンパイル時の追加オーバーヘッドやテストコードの複雑化を招き得る。
- Sleepy Test (ST): スレッドをスリープさせるテストであり、レースコンディションのような望ましくない挙動につながり得る。
- 参照
-
SNUTS.js
- 時期: 2024年
- JavaScriptの言語特性を考慮したテストスメルを検知するツール。デモ動画
- 検知対象
- Anonymous Test: テスト名が機能や目的について説明的でない状態のテスト
- Comments Only Test: テストまたはテストブロックが完全にコメントアウトされているテスト
- General Fixture: テストセットアップが複数のデータやオブジェクトを定義しているが、実際にはその一部しか使用していないテスト
- Overcommented: 過剰にコメントされているテスト
- Sensitive Equality: toString メソッドを使用したアサーションが行われるテスト
- Test Without Description: 説明文が欠けているテストケース
- Transcripting Test: テストブロック内でconsole.log、console.warn、console.errorなどの出力コマンドを使用するテスト
- Complex Snapshot: 複雑なスナップショットテストがある(使用しているスナップショットのサイズが大きすぎる)
- Conditional Test Logic: テストコードに条件分がある
- Identical Test Description: 同じ説明をもつ複数のテストケースを検出する
- Non-Functional Statement: 何も実行しないテストケースを検出する
- Only Test: only修飾語がついて、そのテストケースしか実行されない状態を検出する
- Suboptimal Assert: 最適ではないアサートが含まれている
- Verbose Test: 過度に長く冗長なテスト
- Verifying in Setup Method:テストクラスのセットアップメソッド内でのアサーションメソッド。
- 参照
SteelとSNUTS.jsは検出する対象が異なっているため、網羅的に検知する場合は両方使用する必要があります。
なお、SNUTS.jsは検知用のWebサーバーを起動して、パブリックのGitHubリポジトリを指定するというインターフェイスになっているため、状況によってはそのまま使用できません。
もし、SNUTS.jsの検知機能を直接使用するには以下のようなスクリプトを追加することで対応は可能です。
import astService from "./src/services/ast.service.js";
import detectSensitiveEquality from "./src/common/detectors/sensitiveEquality.js";
import detectAnonymousTest from "./src/common/detectors/anonymousTest.js";
import detectCommentsOnlyTest from "./src/common/detectors/commentsOnlyTest.js";
import detectGeneralFixture from "./src/common/detectors/generalFixture.js";
import detectTestWithoutDescription from "./src/common/detectors/testWithoutDescription.js";
import detectTranscriptingTest from "./src/common/detectors/transcriptingTest.js";
import detectOvercommentedTest from "./src/common/detectors/overcommented.js";
import detectIdenticalTestDescription from "./src/common/detectors/identicalTestDescription.js";
import detectComplexSnapshot from "./src/common/detectors/complexSnapshot.js";
import detectConditionalTestLogic from "./src/common/detectors/conditionalTestLogic.js";
import detectNonFunctionalStatement from "./src/common/detectors/nonFunctionalStatement.js";
import detectOnlyTest from "./src/common/detectors/onlyTest.js";
import detectSubOptimalAssert from "./src/common/detectors/subOptimalAssert.js";
import detectVerboseTest from "./src/common/detectors/verboseTest.js";
import detectVerifyInSetup from "./src/common/detectors/verifyInSetup.js";
const detectors = [
detectAnonymousTest,
detectSensitiveEquality,
detectCommentsOnlyTest,
detectGeneralFixture,
detectTestWithoutDescription,
detectTranscriptingTest,
detectOvercommentedTest,
detectIdenticalTestDescription,
detectComplexSnapshot,
detectConditionalTestLogic,
detectNonFunctionalStatement,
detectOnlyTest,
detectSubOptimalAssert,
detectVerboseTest,
detectVerifyInSetup,
];
const code = `
test("some test", () =>{
expect(10).toBe(10)
})
it("sum",() =>{
expect(10).toBe(10)
})
it.only("x",() =>{
})
`;
const ast = astService.parseCodeToAst(code);
const results = detectors.map((detector) => {
return {name: detector.name, data: detector(ast)};
});
console.log(JSON.stringify(results, null, 2));
複数言語対応したテストスメルを検出
特定のプログラミング言語に依存しないツールや複数言語に対応したツールについて紹介します。
-
TestQ
- 時期:2008
- CppUnit、JUnitに対して解析を行うデスクトップアプリ。ダウンロード可能なtestQ-0.5.tar.bz2がPython2.0で実装されているように見えるので現在使用するのは厳しいと考えられる。
- 検知対象
- Assertionless(AL): データと機能をアサートするように動作しているが、実際にはアサートしていないテスト。
- Assertion Roulette(AR)
- Duplicated Code(DC)
- Empty Test(EmT): 空のテストメソッド
- Eager Test (ET)
- For Testers Only(FTO):
- General Fixture(GF)
- Indented Test(InT): 多数の分岐点・ループ・条件文を含むテストメソッド。
- Indirect Testing(IT)
- Mystery Guest(MG)
- Sensitive Equality(SE)
- Verbose Test(VT): 複雑で、単純でもクリーンでもないテストコード。SLOCが大きいものをそうみなしている。
- 参照
-
AromaDr
- 時期:2025
- 複数の言語に対応したテストスメル検知ツール。現在、C#、Java、JavaScript、TypeScript、Pythonに対応している。
- 検知対象
- Assertion Roulette
- Conditional Test Logic
- Duplicate Assert
- Empty Test
- Exception Handling
- Ignored Test
- Magic Number
- Redundant Print
- Sleepy Test
- Unknown Test
- 参照
機械学習を利用したテストスメルの検知
2022年頃にMachine Learning-Based Test Smell Detectionで機械学習を使用したテストスメルの検知の実験が行われましたが、この実験の範囲ではF値が51%を超えることはありませんでした。
LLMをテストスメルに活用した事例もいくつかあります。
-
- テストスメルをLLMで検出できるかの実験。実プロジェクトでの検出器としての有効性を実験したわけではない。
-
- PythonとJavaのテストスメルの検知と修正をLLMで行えるかを実験した内容。PyNose / TsDetectでテストスメルを抽出したのち、それをLLMで検知できるかを調べている。Geminiで(Python 74.35%、Java 80.32%)の精度で検知できたとある。
-
- JavaScriptを対象にSNUTS.jsとSTEELで検出したテストスメルをLLMを使用してリファクタリングする
手動テストのテストスメル
本来、テストスメルは自動テストの文脈で語られてきましたが、それを手動テストにまで広げた文献もいくつか存在します。
- 2013年 Hunting for smells in natural language tests
- テストスメルの概念を自然言語のテストに広げた。以下の7つを提唱した。
- Hard-Coded Values: テストに多数の「マジックナンバー」や文字列(例:テストデータやUI要素名)が含まれている。
- Long Test Steps: 1つのテストステップが非常に長い
- Conditional Tests: 自然言語で条件ロジックが書かれている
- Badly Structured Test Suite: テストスイートの構造が、テスト対象の機能の構造に対応していない
- Test Clones: テストに類似部分が含まれる
- Ambiguous Tests: テストの記述が不十分で解釈の余地がある
- Inconsistent Wording: 用語が一貫して使われていない
- テストスメルの概念を自然言語のテストに広げた。以下の7つを提唱した。
- 2022年 NALABS: Detecting Bad Smells in Natural Language Requirements and Test Specifications
- NALABSはExcelファイルから要件を読み取り、テストスメルを検出するツール
- 以下の観点で抽出を行う
- Vagueness(曖昧さ):may / could / should have … など、意味を曖昧にする語句の出現。
- Referenceability(参照の多さ):他文書・図表等への参照表現の頻度(NR1/NR2の2指標)。
- Optionality(任意性):can / may / optionally など、解釈の幅を生む語句の使用。
- Subjectivity(主観性):better / worse / as possible など主観・評価語の使用。
- Weakness(弱い表現):adequate / as appropriate / be able to … など不確実・解釈の余地を残す語句。
- Readability(可読性):自動可読性指数(ARI)で計測。
- Complexity(複雑さ):語数(サイズ)や接続詞の出現数などによる複雑度。
- 2023年 Manual Tests Do Smell! Cataloging and Identifying Natural Language Test Smells
- 手動テストのためのテストスメルのカタログと、それを検出するツールを作成した[4]とある。
- この文献で定義したテストスメルは以下の通り
- Ambiguous Test: テスト手順が解釈の余地を残す(曖昧語や不定詞・不定代名詞、比較級・最上級、副詞など)ため、理解や実行に悪影響を与える。著者らは従来の「曖昧語リスト」依存から、形態素・統語に基づくより一般化された規則へ拡張している。
- Conditional Test: 自然言語で条件分岐(if等)を含む記述。意図の把握が難しく、テストの複雑化・保守性低下・誤り誘発につながる。識別では従属接続詞の出現を手がかりにする。
- Eager Action: 1つの手順に複数の行為(命令形動詞)を束ねることで、個々の行為に対する検証抜けを招き、効果的な検証を妨げる。
- Misplaced Action: 本来は行為(Action)欄に書くべき内容が結果/検証(Result/Verification)欄に書かれているなど、構造が崩れたテスト。
- Misplaced Precondition: 前提(Preconditions)であるべき記述が行為(Action)欄に置かれるなど、セクション間の記述位置の取り違え。
- Misplaced Verification: 検証(Verification)が行為(Action)欄に記されるといった誤配置により、構造化・追跡性が損なわれる。
- Tacit Knowledge: 略語や専門語が説明なく使われるなど、読者の前提知識に過度に依存してしまう状態。理解・再現性・保守性を下げる。
- Unverified Action: 行為に対応する検証が存在しないため、実施しても成否が判定できず、テストの有効性を損ねる。
- 2025年 Investigating the Performance of Small Language Models in Detecting Test Smells in Manual Test Cases
- Small Language Models(SLMs)によるテスト・スメル自動検出の実験。
- Phi-4 が最良:pass@2 = 97%となった[5]
手動テストのテストスメルの検知ツールは英語で評価・実験されています。
日本語圏でそのままの精度では使用できないという点については留意してください。
まとめ
本記事ではテストスメルについて紹介をおこないました。
テストスメルは自動テスト・手動テストがうまくいかない兆候を表すものであり、いくつかの自動検知ツールも存在します。
自動テストのテストスメル検知するツールについては、コードスメルの検知に焦点を当てているものが多く、Behavior Smells や Project Smellsに言及したものは少ないです。
Behavior SmellsについてはFlaky Testsや擬似テスト済みメソッドを検知する手法もあわせてみる必要があると思われます。
Project Smellsについては、CIのログ、CI失敗後対応プルリクエスト、タスクの所要時間を上手く監視する必要があるでしょう。
手動テストのテストスメルの検出についてはKiwi TCMSなどのテスト管理ツールよりテストケースとテスト実行情報の一元管理が必須になると思われます。
-
xUnit Test Patterns: Refactoring Test Codeではフィクスチャ(Fixture)を次のように定義しています。
Fixture:
In xUnit, we call everything we need in place to exercise the SUT the test fixture, and we call the part of the test logic that we execute to set it up the fixture setup phase of the test.
xUnitでは、SUT(テスト対象のシステム) を実行するために必要なものすべてをテストフィクスチャと呼び、テストロジックのうち、それをセットアップするために実行する部分をテストのフィクスチャセットアップフェーズと呼びます。
おそらくこれは、“test fixture”(被試験体を一定条件で測るための治具・段取り)が語源となっていると思われます。xUnit 系の源流といわれるKent BeckのSimple Smalltalk Testing: With PatternsにもFixtureについての説明がされています。 ↩︎ ↩︎ -
PyNose: A Test Smell Detector For Python中の参考資料としてSuboptimal Assertが以下のように紹介されている。
↩︎ -
Programming Paradigms BlendはHandling Test Smells in Python: Results from a Mixed-Method Studyで新しく定義されたテストスメル。同一のテストコード内で開発者がオブジェクト指向(OO)と手続き型のパラダイムを混在させているテストコードをProgramming Paradigms Blendとしている。 ↩︎
-
文献中にManual Test Senseiと書いてあるが、その配布先は不明。Manual Test Senseiという同名の手動テストのテストスメルを検知するツールはあるが同じものかは確証がない。 ↩︎
-
スメルのある文を正しく指摘できたかを1サンプル当たり複数回答のうち正答が含まれるかで評価している。Precision/Recall/F1のようなデータセット全体の実運用精度は未提示なので過剰にいい数値になっている可能性が高い。 ↩︎
Discussion