🙌

「単体テストの考え方/使い方」勉強会01 〜理想的なテストと壊れやすいテスト〜

2025/01/31に公開

「単体テストの考え方/使い方」勉強会01 〜理想的なテストと壊れやすいテスト〜

Specteeでエンジニアをしている峯田、國久、和山、永野です。

今回は、会社で勉強している「単体テストの考え方/使い方」をテーマに、テスト設計の重要なポイントをお伝えします。特に、理想的なテスト極端なテストに焦点を当てて、それぞれの特徴や違いについて簡単にご紹介します。

理想的なテストとは?

良い単体テストの4つの柱(概略)

  • 退行に対する保護:変更が既存の機能を壊さないことを確認する
  • リファクタリング耐性:コードの内部構造が変更されてもテストが失敗しない
  • 迅速なフィードバック:テストの実行が速く、すぐに結果が得られる
  • 保守のしやすさ:テストコードが理解しやすく修正しやすい

理想的なテストとは、「良い単体テストを構成する4本の柱をすべて完全に備えたテスト・ケース」です。この定義を前提として、テストケースの価値は「4つの柱の掛け算」により評価されます。そのため、柱がどれか1本でも完全に欠如しているテストの価値は「0」になるということに注意が必要です。

また、4本のうち3本の柱(退行に対する保護、リファクタリング耐性、迅速なフィードバック)は互いに排反する性質(1本の柱を最大限に備えると他の柱が犠牲になる)を持つため、全てを完全に備えたテストは作成できません。つまり、どの柱を優先し犠牲にするかというトレード・オフの決断と、4本の柱のどれかが完全に欠如したテスト・ケースを作らないようにすることが求められます。

本書では、背反する3本柱のうちリファクタリングへの耐性を最も重要で犠牲にできないものと位置づけています。そのため、退行に対する保護と迅速なフィードバックをトレードオフスライダーとして考えます。

極端なテストとは?

極端なテストとは、上記の3本柱(退行に対する保護、リファクタリング耐性、迅速なフィードバック)のうち2本しか満たさず、1本の完全に欠如した柱があるテストです。
上記のテスト評価定義によると、この極端なテストの価値は「0」と評価されるため、可能な限り避ける必要があります。

今回は、この極端なテストの中で、リファクタリング耐性が欠如した壊れやすいテストに焦点を当て紹介します。

壊れやすいテストの例

壊れやすいテストは、実行時間が短く、退行を見つけることに優れているものの、リファクタリング耐性が欠如しており、多くの偽陽性を持ち込むテストである。

コード例と解説

SQL文の正確な内容を検証しようと内部的な実装に依存している例。

テスト対象のコード
sut.cs
public class UserRepository
{
    public string LastExecutedSqlStatement { get; private set; }

    public User GetById(int id)
    {
        LastExecutedSqlStatement = $"SELECT * FROM dbo.[User] WHERE UserID = {id}";
        // 実際のデータベースアクセスコードは省略
        return new User { Id = id, Name = "John Doe" };
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}
テストコード
test.cs
public class UserRepositoryTests
{
    // 壊れやすいテストの例
    [Fact]
    public void GetById_executes_correct_SQL_code()
    {
        // Arrange: UserRepositoryオブジェクトを初期化
        var sut = new UserRepository();

        // Act: GetByIdメソッドを実行
        User user = sut.GetById(5);

        // Assert: 実行されたSQL文が期待されるものと一致するかを検証
        Assert.Equal("SELECT * FROM dbo.[User] WHERE UserID = 5", sut.LastExecutedSqlStatement);
    }
}

迅速なフィードバック

  • 説明: 壊れやすいテストは実行時間が短い。
  • 理由: テスト対象のコードがシンプルであるため、テストの実行が迅速に行える。

退行に対する保護

  • 説明: 壊れやすいテストは退行を見つけることに優れている。
  • 理由: テスト対象のコードが正しい動作をしているかどうかを詳細に検証するため、バグを見つけやすい。

リファクタリングへの耐性

  • 説明: 壊れやすいテストはリファクタリングへの耐性が低い。
  • 理由: テストが内部的な実装に依存しているため、リファクタリングを行うとテストが失敗しやすい。

感想と実践

今回の学習によって、4つの柱「退行に対する保護、リファクタリング耐性、迅速なフィードバック、保守のしやすさ」を意識したより価値の高い単体テストを増やせるようになりました。

一方、壊れやすいテストを例とした価値の低い極端なテストを認知しやすくなり、このような価値の低い負債のようなテストを削減できたことも良かったです。

今後は、価値のある統合テストおよび、価値のある単体・統合・E2Eテストの組み合わせを探求していこうと思います。

ブログ協力者

Spectee Developers Blog

Discussion