🗑️

テストも捨てやすく作ろう

に公開

はじめに

この記事は「株式会社カオナビAdvent Calendar 2025」の6日目の記事です。

昨年、株式会社カオナビの佐野さんが「ソフトウェアは捨てやすく作ろう」という記事を執筆しました。
https://qiita.com/sanogemaru/items/40557c6db33dcec99cf1

今回、その内容を踏まえ、ソフトウェアテストに焦点を当てて「捨てやすいテスト」について考察します。
また、最後に「テストアーキテクチャ」についても触れ、さらなる学びへの道筋を示したいと思います。

「捨てやすいテスト」とは

まず、本記事における「捨てやすいテスト」の立ち位置を明確にしておきます。

「捨てやすいテスト」とは、「積極的にテストを削除すべき」という意味ではありません。

むしろ、以下のような状態を指します。

  • 各テストの目的と責務が明確で、「なぜこのテストが必要か」を説明できる
  • テストが不要となった際に、削除の判断を迷わずできる
  • テストの追加・修正・削除が、他のテストに影響を与えにくい

つまり、「必要なテストは残し、不要なテストは迷わず削除できる健全な状態」を保つことを意味します。

この記事では、あえて「捨てやすい」という表現を使いましたが、テストのリファクタリングについても同様に考えることができます。
その上でお読みください。

どうして捨てやすく作るのがいいのか

仮説検証を繰り返しながらソフトウェアを開発する場合に、プロダクトコードを捨てることを前提に設計することは重要であることについて、前述のブログで佐野さんが述べました。
同様に、テストコードも捨てやすく作ることが重要です。

これは、QAエンジニアとして、以下の考えがあるためです。

  1. テストはユーザーへの価値を直接的に生まないものである
  2. CIで実行される自動テストの場合、コンピューターリソースを消費する
  3. 捨てやすい状態を維持することで、本質的な品質保証に集中できる

1.テストはユーザーへの価値を直接的に生まないものである

この考えは、一見受け入れがたい考えかもしれません。
実際に私はテスターとして、テストによる品質保証は必要不可欠なものだと考えています。
ただし、それは「必要かつ説明責任を果たせるテストがある」ことの重要性であって、「どんなテストでも良い」という意味ではありません。

テストを行なっても「ユーザーから見た製品の振る舞いは変わらない」ことは事実です。
テストの価値の1つは、ユーザーがバグと出会うのを防ぐことです。つまり、テストは間接的にユーザー価値を生み出すものです。

プロダクトコードは直接的にユーザー価値を生み出しますが、テストは違います。
だからこそ、テストについては「本当にこのテストが必要か」と厳しく問い続ける姿勢が重要だと考えています。

私がテスターとして大切にしていることは、「最小限のテストで最大限の品質保証を実施する」です。
これは安易な妥協を意味するのではありません。
むしろ「本当に必要なテストは何か」を常に考え抜くことを意味すると考えています。

この論拠に対する注意点

ただし、これはテストにおける品質保証の側面と開発の側面を慎重に区別する必要があります。
例えばTDDやBDDのように、テストが設計や開発の一部として機能する場合、テストは開発プロセスにおいて重要な役割を果たします。
今回、品質保証の手段としてのテストに焦点を当てていることに注意してください。

開発の側面を考えたとき、テストは「変わらないことを保証する」というアジリティを支える重要な開発インフラです。
だからこそ、そのコンテキストにおいて慎重に設計され、維持されるべきです。

テストの二面性についてはさまざまなところで議論されています。
私も書いています。
気になった方はぜひ、以下の記事を参考にしてください。
https://zenn.dev/55_ymzn/articles/software_test_duality

また、開発者にとってのテスト、特にプログラマーが実装中に考える「捨てやすいテスト」をどのように捉えるかについては、マイブラザーのスタヰル(@stwile871)が執筆しています。
https://volare-viah.com/blog/s5hfq2jwkl1
そちらもぜひご覧ください。

2.CIで実行される自動テストの場合、コンピューターリソースを消費する

CIで実行される自動テストは、コンピューターリソースを消費します。
特にE2Eテストは実行に時間がかかり、CIのパイプライン全体の速度や並列化のコストに影響を与えます。
そのため、「捨てやすい状態」を目指すことは、開発の効率化やスピードを目指す上でとても重要です。

3.捨てやすい状態を維持することで、本質的な品質保証に集中できる

「テストが捨てやすい」とは一見矛盾しているように思えるかもしれませんが、そうではないと考えています。

自動テストが肥大化した組織にとってよくある問題が、「何かあると不安だから残しておこう」という思考です。
これは、人間として非常に理解できる考えではありますが、テスターとしては避けるべきです。
「不安」という考えは、非常に主観的で曖昧です。

「捨てやすい状態」とは、各テストの目的と責務が明確で、不要となったときに迷わず削除できる状態を指します。
この状態を維持することで、「不安」という曖昧な理由ではなく、明確な根拠に基づいてテストの価値を判断できるようになります。
これこそが、品質に対する真の責任感につながると考えています。

捨てやすくするために取り組むべきこと

では「捨てやすいテスト」とはどういったものでしょうか?
私の個人的な答えとして「テストの責務が明確である」ことが重要だと考えています。

ソフトウェアテストの原則として、「テストは欠陥があることは示せるが、欠陥がないことは示せない」(JSTQB FLV4.0より)というものがあります。
この原則を踏まえると、基本的な姿勢として、テストは明確な目的があり、その目的に対して責務を果たすものであるべきです。

これに加えて、私はテストにおいて「カバレッジする」という概念を重要視しています。
詳しくは以下の記事をご覧ください。
https://zenn.dev/55_ymzn/books/what_is_testing/viewer/05_what_is_test_techniques#超重要な「カバレッジ」という概念

ここでいう「カバレッジ」とは、コードカバレッジ(行数や分岐の網羅率)のことではありません。
「テスト観点のカバレッジ」、つまり「特定のスコープに対して妥当なテスト観点でテストケースを設計し、必要な検証を漏れなく行なうこと」を指します。

そう考えると、テストケースの一側面として以下のようにあるべきだと考えられます。
「テストケースによって得られる結果に意味があること」

これは複数のテストケースで1つのテスト条件を満たす場合もあれば、1つのテストケースで1つのテスト条件を満たす場合もあります。

なんにせよ「このテストケースはこの責務を持っている」と識別できることが重要なのです。

具体的なテストコードの例

ここまでは抽象論としてテストコードの捨てやすさについて述べてきました。
ここまで挙げてきたような捨てやすいテストを全体観を持ってイメージすることは、ある程度経験を積んだQAエンジニアでも難しいことがあります。

ここからは最もイメージしやすい具体的なテストコードの例を挙げて、捨てやすいテストと捨てにくいテストの違いを説明したいと思います。

今回は例として、E2Eテストを考えてみましょう。
PythonでPlaywrightを使用している例を記載します。

1. 責務を分割する

E2Eテストにおいて、ユーザーフローをすべて検証するテストケースを書くことはよくあります。
これはコンテキストによっては有効な場合もありますが、一般的には捨てにくいテストケースになりがちです。

この文章の冒頭で記述した捨てやすいテストである必要性を考えた上で、このようなテストケースが本当に必要かどうかは考えるべきです。
例えば、以下のような形で責務を分割した方が捨てやすいテストになることが多いです。

捨てにくいテスト
def test_complete_flow(page: Page):
    """ヘルパー関数で実装したログイン、プロフィール編集、商品購入、ログアウトを全部検証"""
    login(page)
    edit_profile(page)
    search_and_buy(page)
    logout(page)
    # ... このあと長く続く
捨てやすいテスト
def test_login_redirects_to_dashboard(page: Page):
    """責務: ログイン後、ダッシュボードへリダイレクトされること"""
    page.goto("/login")
    page.fill("#username", "test_user")
    page.fill("#password", "test_password")
    page.click("button[type='submit']")
    
    expect(page).to_have_url("/dashboard")

def test_profile_update_shows_success_message(page: Page):
    """責務: プロフィール更新時、成功メッセージが表示されること"""
    login_as_test_user(page)
    page.goto("/profile/edit")
    
    page.fill("#bio", "Updated bio text")
    page.click("button[type='submit']")
    
    expect(page.locator(".success-message")).to_contain_text("更新しました")

補足: Page Object Model との関連性

ここで紹介した「捨てやすいテスト」の例では、login_as_test_user(page) のように、特定の操作(この場合はログイン)をヘルパー関数としてカプセル化しています。
これは、E2Eテストの設計パターンとして有名なPage Object Model (POM) の考え方に通じるものです。
奇しくも、この記事が公開される本日(2025/12/6)、私はテスト自動化カンファレンスにて、Page Object Modelと関心事の分離について発表します。

「捨てにくいテスト」の例は、テストケース自体が複数の検証責務を持っていました。
一方で「捨てやすいテスト」は、テストケースの責務を1つに絞り、UI操作の詳細を login_as_test_user のような(あるいはPage Objectのような)別の場所に分離しています。

このように関心事を分離することで、例えばログイン画面のUIが変更された場合、login_as_test_user という1か所を修正するだけで、それを利用するすべてのテストケースを修正せずに済みます

責務が分割され、独立性が高ければ、「コストや期日などの制約でテストを減らしたい」とニーズが出た際に有効です。
他のテストへの影響の懸念を小さくしつつ、そのテストケースあるいはテストスイートだけを安全に削除ができます。
これが、テストコードがスパゲッティ化していると、「消すと他が壊れるかもしれないから、無駄だけど残しておこう」という状態に陥ってしまいます。

これは、本記事で述べている「責務が明確で、修正が他に影響を与えにくい」という「捨てやすいテスト」の状態を実現するためのテクニックです。

2. テストケース名とAssertで目的を明示する

E2Eテストにおいて、テストケース名やAssert文で何を検証しているのかが不明瞭な場合があります。
これは捨てにくいテストケースの典型例です。

特にE2Eテストはテスト対象のシステムと分離したテストコードになることが多く、テストケース名などで目的を明示しないと、後から見たときに何を検証しているのかわからなくなることが多いです。
テストケースのメソッド名については日本語で書くことも推奨される場合があります。
コンテキストによって使い分けて、明確に記載することを心がけるといいでしょう。

捨てにくいテスト
def test_user_scenario(page: Page):
    """何を検証したいのか不明"""
    page.goto("/dashboard")
    page.click("#some-button")
    # ... 色々な操作が続く
捨てやすいテスト
def test_invalid_login_shows_error_message(page: Page):
    """責務: 無効な認証情報でログイン時、エラーメッセージが表示されること"""
    page.goto("/login")
    page.fill("#username", "test_user")
    page.fill("#password", "invalid_password")
    page.click("button[type='submit']")
    
    expect(page.locator(".error-message")).to_have_text("認証に失敗しました")

3.依存関係の連鎖

E2Eテストにおいて、ある機能の結果を使って次の機能をテストし、その結果でさらに次の機能をテストする、という形で依存関係が連鎖している場合があります。
これも捨てにくいテストケースの典型例です。
ある機能で失敗すると、その後の機能のテストもすべて失敗してしまい、後続のテストが続かなくなり、効率的なテストと問題の特定が難しくなります。

可能な限り、各テストケースが独立して実行できるように設計することが望ましいです。

捨てにくいテスト
def test_article_crud_flow(page: Page):
    """記事の作成・編集・削除を一連のフローとして検証"""
    # 記事を作成
    page.goto("/articles/new")
    page.fill("#title", "test article")
    page.fill("#content", "test content")
    page.click("#publish-button")
    article_id = page.locator("#article-id").text_content()
    
    # 作成した記事を編集(作成の成功に依存)
    page.goto(f"/articles/{article_id}/edit")
    page.fill("#title", "updated title")
    page.click("#update-button")
    expect(page.locator(".success-message")).to_contain_text("更新しました")
    
    # 編集した記事を削除(作成と編集の成功に依存)
    page.goto(f"/articles/{article_id}")
    page.click("#delete-button")
    page.click("#confirm-delete")
    expect(page.locator(".success-message")).to_contain_text("削除しました")
捨てやすいテスト
def test_created_article_can_be_viewed(page: Page):
    """責務: 作成した記事が閲覧できること"""
    page.goto("/articles/new")
    page.fill("#title", "new article")
    page.fill("#content", "test content")
    page.click("#publish-button")
    
    # 作成した記事が実際に閲覧できることを確認
    article_url = page.url
    page.goto(article_url)
    expect(page.locator("[data-testid='article-title']")).to_have_text("new article")

def test_article_title_can_be_updated(page: Page):
    """責務: 記事のタイトルを更新できること"""
    article_id = create_test_article()
    
    page.goto(f"/articles/{article_id}/edit")
    page.fill("#title", "updated title")
    page.click("#update-button")
    
    # 更新内容が実際に反映されていることを確認
    page.goto(f"/articles/{article_id}")
    expect(page.locator("[data-testid='article-title']")).to_have_text("updated title")

def test_deleted_article_cannot_be_accessed(page: Page):
    """責務: 削除した記事にアクセスできなくなること"""
    article_id = create_test_article()
    
    page.goto(f"/articles/{article_id}")
    page.click("#delete-button")
    page.click("#confirm-delete")
    
    # 削除後、その記事にアクセスできないことを確認
    response = page.goto(f"/articles/{article_id}")
    expect(response.status).to_be(404)

補足: 独立性を高めるセットアップと事後処理

「独立したテスト」を実現するには、各テストで必要な前提条件を、他のテストの実行結果に依存しないようにセットアップすることが重要です。

def create_test_article() -> str:
    """
    テスト用の記事を作成する
    
    Returns:
        str: 作成された記事のID
    """
    response = requests.post(
        f"{TEST_API_BASE_URL}/articles",
        json={"title": "test", "content": "test content", "status": "published"}
    )
    return response.json()["id"]

これにより、各テストは独立して実行でき、かつユーザーにとって本当に重要な動作を検証できます。
※実際には、APIのエラーハンドリングやエンドポイントや認証情報など、コンテキストに応じて適切に実装してください。

また、テストの独立性を保つためには「事後処理(Teardown)」も同様に重要です。
テスト実行によって作成されたデータが環境に残ったままだと、それがノイズとなり、他のテスト結果に予期せぬ影響を与えてしまう(=依存関係が生まれてしまう)ことがあります。
「いつ、どのテストを削除しても、他のテストは正常に動作する」という捨てやすい状態を保つために、作成したデータをクリーンアップする仕組みも合わせて検討することをお勧めします。

「テストアーキテクチャ」という考え方

捨てやすいテストを考える上で、重要な概念として知っておいてほしいのが「テストアーキテクチャ」という考え方です。

「テストアーキテクチャ」という言葉自体は、さまざまな解釈がありますが、ここでは「テスト設計コンテスト」や「VSTeP」を参考にした捉え方をしたいと思います。

興味がある方は、以下のテスト設計コンテストのページから「テスト設計チュートリアル」をぜひご覧ください。
動画と資料が存在します。
https://www.aster.or.jp/testcontest/index.html

それは、「テストをする理由(テスト観点)の全体像とテストケースに記述されるまでの意図を示したもの」です。
※これは資料のなかで明示的に示されたものではなく、私個人の解釈です。コンテナモデリングとテストフレームモデリングを「テストアーキテクチャ設計」と捉えている場合の解釈です。

テストアーキテクチャは 高凝集・疎結合 であり、また組織のコンテキストに適応したものがよいとされています。

高凝集・疎結合なテストアーキテクチャは、まさに「捨てやすいテスト」の条件と同じだと考えます。

  • 高凝集: 各テストの責務が明確で、目的が集約されている
  • 疎結合: テスト同士の依存関係が少なく、独立して削除・修正できる

また、テストアーキテクチャはそのコンテキストに合わせて、説明責任を果たすものであるべきです。
つまり、「なぜこのテストが必要なのか」「なぜこのテストを削除できるのか」を明確に説明できる状態を保つことが重要です。

Advanced Level:アーキテクチャと実装における品質特性のトレードオフ

最後に1つ、重要な視点を補足しておきます。

アーキテクチャとは、要求、特に「品質特性」に関連する要求を満たすように設計されるものです。

本記事でテーマにした「捨てやすさ」は、品質特性で言うところの「保守性」を高めるためのアプローチです。
一方で、これを追求することで「性能効率性」などといった他の品質特性とトレードオフになる場合もあります。

実際、記事の前半で触れた「CIのリソース消費」などは、まさにこのバランス感覚が問われる部分です。

特に今回の例に出したテストコードなどの「テスト実装」の場合、例えばアーキテクチャとして「捨てやすさ」を保ちつつ、実装では別の品質特性をカバーするなどの戦略もあります。
プロダクトの特性や事業の状況に合わせてこれら品質特性のトレードオフを意識し、バランスを取っていくこともまた大切です。

Special Thanks
https://x.com/hide_ramen_san/status/1997101950484250806?s=20

おわりに

「捨てやすさ」という佐野さんの視点をテストに適用して論じてきましたが、実のところ、本質は別のところにあります。
責務が明確なテストアーキテクチャの構築が目指す姿であるという点です。

その結果として「捨てやすいテスト」が実現されると考えます。

しかしながら、今のテストアーキテクチャ・あるいは最も具体的な成果物のテストコードの健全さを考えるにあたり、
「このテストは捨てやすいだろうか?」という問いは有効だと考えます。

GitHubで編集を提案

Discussion