テストコードの書き方入門|Arrange-Act-Assert(AAA)パターンで単体テストの基本構造を理解する

pytest でテストを書き始めると、「テストコードってどう構成すればいいの?」と悩むことがあります。そこで覚えておきたいのが Arrange-Act-Assert(AAAパターン) です。「準備 → 実行 → 確認」の3段構造で書くだけで、読みやすく・保守しやすいテストコードが書けるようになります。

📌 この記事の対象読者

✅ pytest を使い始めたばかりで、テストコードの書き方がわからない方
✅ テストを書いてみたが、読みにくい・何をテストしているかわかりにくいと感じる方
✅ 「良いテストコード」の基準を知りたい QA エンジニア・開発者
pytest-guide に進む前に基礎を固めたい方

✅ この記事を読むと得られること

  • AAAパターン(Arrange・Act・Assert)の役割と書き方が理解できる
  • 「1テスト1目的」の原則と、読みやすいテスト関数名の付け方がわかる
  • pytest での実践的なテストコード例が手に入る
  • よくある間違いとその直し方がわかる

👨‍💻 この記事の信頼性について

本記事は QA エンジニアとして15年以上、テスト自動化に携わってきた筆者が執筆しています。「テストは書けるが、チームメンバーに読んでもらえない」という経験から、AAAパターンの重要性を実感し、現在の現場でも標準として採用しています。

📌 この記事の結論

  • テストコードは Arrange(準備)→ Act(実行)→ Assert(検証) の3段構造で書く
  • 1つのテスト関数で 1つの目的だけ を確認するのが基本
  • 関数名に 何を・どんな条件で・何が起きるか を含めると可読性が上がる

テストコードを書き始めた頃、筆者もこんなコードを書いていました。

# ❌ 構造がなく、何をテストしているかわかりにくい
def test_calc():
    assert add(1, 2) == 3
    assert add(-1, 1) == 0
    assert add(0, 0) == 0
    result = add(100, 200)
    assert result == 300
    assert add(1.5, 2.5) == 4.0

一見問題なさそうに見えますが、どれかが失敗したとき「何のテストが失敗したのか」すぐにわかりません。また複数の検証が混在しているため、テストの意図が伝わりにくいです。この問題を解決するのが Arrange-Act-Assert パターン です。

Arrange-Act-Assert(AAA)パターンとは

AAAパターンとは、テストコードを 3つのフェーズ に分けて書く構造のことです。どんなプログラミング言語・テストフレームワークでも使える普遍的なパターンで、テスト界隈では事実上の標準として広く使われています。

Arrange
準備
Act
実行
Assert
検証
フェーズ役割具体的にやること
Arrange(準備)テストに必要な状態を整える変数の定義、オブジェクト生成、mock の設定など
Act(実行)テスト対象の処理を1回だけ実行する関数呼び出し、API リクエスト、ボタンクリックなど
Assert(検証)結果が期待通りかを確認するassert 文で値・状態・例外などを検証

AAAパターンの基本形

先ほどのテストを AAAパターンで書き直すとこうなります。

# ✅ AAAパターンで書いた場合
def test_add_returns_correct_sum_with_positive_numbers():
    # Arrange(準備): テストに必要なデータを用意する
    a = 2
    b = 3

    # Act(実行): テスト対象の処理を1回実行する
    result = add(a, b)

    # Assert(検証): 結果が期待通りか確認する
    assert result == 5


def test_add_returns_zero_when_adding_opposite_numbers():
    # Arrange
    a = -1
    b = 1

    # Act
    result = add(a, b)

    # Assert
    assert result == 0

💡 何が変わったか?

  • 1つの関数が1つのケースだけをテストするようになった
  • コメント(# Arrange)で構造が一目でわかるようになった
  • テスト関数名を読むだけで「何をテストしているか」がわかる
  • 失敗したとき、どのケースが壊れたかが即座に特定できる

各フェーズの詳細と書き方

① Arrange(準備):テストの前提を作る

Arrange フェーズでは、テストに必要な変数・オブジェクト・データを用意します。このフェーズが長くなる場合は pytest fixture に切り出すと整理できます。

def test_user_full_name():
    # Arrange: テストに必要なオブジェクトを生成する
    user = User(first_name="田中", last_name="太郎")

    # Act
    result = user.get_full_name()

    # Assert
    assert result == "田中 太郎"

⚠️ Arrange が長くなりすぎるときのサイン

Arrange が10行を超えるようなら、テストが複雑すぎるか、fixture に切り出すことを検討しましょう。準備が長いとテストの本質(何を確認したいか)が埋もれてしまいます。

💡 AAAパターンと fixture の関係

fixture は AAA を置き換えるものではなく、Arrange 部分を複数テストで共通化するための仕組みです。

Arrange
→ fixture で共通化
Act
テスト対象を実行
Assert
結果を検証

② Act(実行):検証したい処理を実行する

Act フェーズでは、検証したい処理を実行します。理想的には「1つの目的に絞った操作」ですが、実務ではログイン後に操作する・カートに追加してから合計を取得するなど、複数の手順が必要なケースも普通にあります。その場合でも「このテストで何を検証したいのか」が明確になるよう意識しましょう。

# ⚠️ Arrange と Act が混在している(構造がわかりにくい)
def test_bad_example():
    cart = ShoppingCart()
    cart.add_item("apple", price=100)
    cart.add_item("banana", price=150)
    cart.apply_discount(10)       # ← 割引もテストしたいのか?合計だけ?
    result = cart.total()
    assert result == 225

# ✅ 何を検証するかを明確にした書き方
def test_total_is_correct_after_adding_two_items():
    # Arrange(前提条件:カートに2商品が入っている)
    cart = ShoppingCart()
    cart.add_item("apple", price=100)
    cart.add_item("banana", price=150)

    # Act(検証対象:合計金額の取得)
    result = cart.total()

    # Assert
    assert result == 250

③ Assert(検証):期待値と実際の結果を比較する

Assert フェーズでは assert 文で結果を検証します。1テスト関数につき assert は1〜3個程度が目安です。pytest は assert 文の失敗時に詳細な差分を自動で表示してくれるため、複雑なアサーションライブラリがなくてもシンプルに書くことができます。

# ✅ シンプルな assert(推奨)
assert result == 5
assert result is not None
assert "error" not in response_body
assert status_code in (200, 201)

# ✅ エラーメッセージ付き(失敗時に原因がわかりやすい)
assert result == 5, f"期待値: 5, 実際の値: {result}"

# ❌ 1つの関数に assert を詰め込みすぎ
def test_user():
    user = create_user("田中", "taro@example.com")
    assert user.name == "田中"
    assert user.email == "taro@example.com"
    assert user.is_active is True
    assert user.role == "user"
    assert user.created_at is not None  # これだけで5つの確認

💡 複数の assert が必要なときは?

ユーザー生成後の「名前・メール・権限・状態」をまとめて確認したい場合は、それぞれを別のテスト関数に分けるか、関連性が強い場合は2〜3個まとめるのが現実的です。「失敗したときに原因が1つに絞れるか」を基準に判断しましょう。

読みやすいテスト関数名の付け方

AAAパターンと同じくらい重要なのが、テスト関数名です。関数名を読むだけで「何をテストしているか」がわかると、失敗時の調査が格段に速くなります。

推奨パターン:test_[何を]_[どんな条件で]_[何が起きるか]

悪い例良い例
test_login()test_login_with_valid_credentials_returns_200()
test_add()test_add_returns_correct_sum_with_positive_numbers()
test_error()test_login_with_wrong_password_raises_auth_error()
test_user_save()test_save_user_with_duplicate_email_raises_value_error()

長い名前に見えるかもしれませんが、テスト結果一覧に並んだとき、関数名だけで「何が失敗したか」が即座にわかります。ログを読む時間が大幅に短縮されます。

💡 複数の条件をテストしたい場合は parametrize が便利

正常系・境界値・異常系など複数のケースを AAAパターンで書くと関数が増えますが、@pytest.mark.parametrize を使うと1つの関数で複数条件をまとめて検証できます。

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (2,  3,   5),   # 正常系
    (0,  0,   0),   # 境界値
    (-1, 1,   0),   # 負の数
])
def test_add_returns_correct_sum(a, b, expected):
    result = add(a, b)          # Act
    assert result == expected   # Assert

詳しくは pytest 完全ガイド を参照してください。

実践:よくあるテストを AAAパターンで書く

例①:計算関数のテスト

def add(a: float, b: float) -> float:
    return a + b


def test_add_returns_correct_sum_with_positive_numbers():
    # Arrange
    a = 3
    b = 4

    # Act
    result = add(a, b)

    # Assert
    assert result == 7


def test_add_returns_negative_when_both_negative():
    # Arrange
    a = -3
    b = -4

    # Act
    result = add(a, b)

    # Assert
    assert result == -7

例②:例外が発生することをテストする

import pytest


def divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("0で割ることはできません")
    return a / b


def test_divide_raises_value_error_when_divisor_is_zero():
    # Arrange
    a = 10
    b = 0

    # Act & Assert(例外テストは Act と Assert が一体になる)
    with pytest.raises(ValueError, match="0で割ることはできません"):
        divide(a, b)

例③:クラスのメソッドをテストする

class ShoppingCart:
    def __init__(self) -> None:
        self._items: list[dict] = []

    def add_item(self, name: str, price: int) -> None:
        self._items.append({"name": name, "price": price})

    def total(self) -> int:
        return sum(item["price"] for item in self._items)


def test_total_returns_sum_of_all_item_prices():
    # Arrange
    cart = ShoppingCart()
    cart.add_item("apple", price=100)
    cart.add_item("banana", price=200)

    # Act
    result = cart.total()

    # Assert
    assert result == 300


def test_total_returns_zero_when_cart_is_empty():
    # Arrange
    cart = ShoppingCart()

    # Act
    result = cart.total()

    # Assert
    assert result == 0

AAAパターンで陥りやすいはまりポイント

🚧 はまりポイント一覧

① Act フェーズに複数の操作を書いてしまう

Act には「テストしたい処理」を1つだけ書きます。複数の操作を書くと、どの操作が原因で失敗したか特定しにくくなります。準備の操作は Arrange に移すのが正解です。

② Arrange の中に assert を書いてしまう

「準備段階でも確認したい」という気持ちはわかりますが、Arrange 内の assert はテストの本題と混同されます。確認は必ず Assert フェーズに集約しましょう。

③ テスト関数名が test_1、test_func など意味のない名前になる

CI/CD でテストが失敗したとき、関数名が唯一の手がかりです。test_add_with_zero_returns_other_value のように「何をテストしているか」を関数名に書くことが大切です。

④ 1つのテストで複数の条件をまとめてしまう

「正常系・異常系・境界値をまとめて1つのテストで確認したい」という気持ちは理解できますが、それぞれ別のテスト関数に分けることで、どの条件が失敗したかが明確になります。@pytest.mark.parametrize を使うと整理しやすいです。

⑤ コメント(# Arrange など)を省略する

慣れてくるとコメントを省略したくなりますが、チーム開発ではコメントがあることで他のメンバーが構造を素早く把握できます。少なくともテストに慣れるまでは省略しない方が安全です。

👨‍💻 実務のコードレビューでは何を見るか

筆者がテストコードをレビューするとき、最初に確認するのは AAAの構造になっているか です。「Arrange がなく Act と Assert だけのコード」や「Arrange に assert が混在しているコード」は、意図が読み取れず後のメンテナンスで混乱しやすいため、まず構造の修正を依頼します。テストコードは「動けばよい」だけでなく、チームの共有ドキュメントとして書くという意識が長期的な品質につながります。

よくある質問(FAQ)

Q. AAAパターンは必ず使わなければいけないですか?

必須ではありませんが、チーム開発・長期的な保守を考えると採用するメリットが大きいです。小規模・個人のスクリプト的なテストでは省略するケースもあります。まずは意識してみることが大切です。

Q. 例外テストで Act と Assert が一体になるのは問題ですか?

問題ありません。pytest.raises() を使った例外テストは Act と Assert が一体化するのが自然です。このような場合は # Act & Assert とコメントを書くと意図が伝わります。

Q. 1テスト関数に assert をいくつまで書いていいですか?

厳格なルールはありませんが、1〜3個程度が目安です。関連性の強い検証(例:レスポンスのステータスコードとボディを両方確認する)はまとめることがあります。ただし assert が増えるほど「どれが失敗原因か」を見つけにくくなります。

Q. AAAパターンと Given-When-Then の違いは何ですか?

概念はほぼ同じです。Given-When-Then は BDD(振る舞い駆動開発)で使われる表現で、より自然言語に近く書けます。どちらを使っても構造は同じです。

AAAパターンGiven-When-Then(BDD)意味
ArrangeGiven〜という状態のとき
ActWhen〜を操作すると
AssertThen〜という結果になる
Q. AAAパターンと TDD(テスト駆動開発)は関係がありますか?

TDD(テスト駆動開発)は「テストを先に書いてからコードを実装する開発手法」です。AAAパターンはテストの書き方の構造であり、TDDと組み合わせて使えます。TDD でテストを書く際も AAAパターンで構造化すると可読性が上がります。

Q. AAAパターンは UI テスト(Selenium・Playwright)でも使えますか?

使えます。UI テストでは Arrange にドライバー起動・ログイン処理などを書き、Act にテストしたい画面操作(ボタンクリック・フォーム入力など)を書き、Assert にページのタイトルや表示内容の確認を書くのが典型的なパターンです。

Q. @pytest.mark.parametrize と AAAパターンは併用できますか?

できます。parametrize はテストデータを外側から渡す仕組みです。@pytest.mark.parametrize でデータを与えつつ、テスト関数内を AAA 構造にすることで「複数条件を整理しながら読みやすいコード」が書けます。複数のテストケースをまとめる際の最初の選択肢として覚えておきましょう。

Q. pytest の fixture は Arrange の代わりになりますか?

はい。テスト間で共通する準備処理(データベース接続・オブジェクト生成など)を fixture として切り出すと Arrange フェーズが短くなり、テストの本質が見やすくなります。fixture は Arrange の「より整理された形」と考えてください。詳しくは pytest 完全ガイド を参照してください。

まとめ

  • AAAパターンは Arrange(準備)→ Act(実行)→ Assert(検証) の3段構造
  • 1つのテスト関数で 1つの目的・1つの検証 を意識する。実務では複数手順が必要なケースもある
  • テスト関数名は test_[何を]_[どんな条件で]_[何が起きるか] の形が読みやすい
  • Assert は1〜3個程度が目安・準備が長くなるときは pytest fixture に切り出す
  • 複数条件のテストは @pytest.mark.parametrize で整理できる

AAAパターンは「難しいテクニック」ではなく、テストコードを読む人への配慮です。将来の自分やチームメンバーが「何をテストしているか」を一目で理解できるコードを書くことが、保守しやすいテストスイートへの第一歩です。まずは次に書くテスト関数から、AAAパターンを意識してみてください。

📚 あわせて読みたい関連記事

タイトルとURLをコピーしました