Skip to content

Elixirテスト完全ガイド

ElixirのテストフレームワークExUnitを使用したテスト実装を、実務で使える実装例とともに詳しく解説します。

ExUnitは、Elixirの標準テストフレームワークです。シンプルで高速なテスト実行を可能にします。

ExUnitの特徴
├─ 並行実行(async: true)
├─ セットアップとクリーンアップ
├─ モックとスタブ
└─ データベーステスト

問題のある構成(テストなし):

# 問題: 手動での動作確認
defmodule MyApp do
def add(a, b) do
a + b
end
end
# 手動でテスト
MyApp.add(1, 2) # => 3(期待通り)
MyApp.add(2, 3) # => 5(期待通り)
# 問題: 回帰テストができない

解決: ExUnitによる自動テスト

# 解決: 自動テスト
defmodule MyAppTest do
use ExUnit.Case, async: true
test "add/2 returns the sum of two numbers" do
assert MyApp.add(1, 2) == 3
assert MyApp.add(2, 3) == 5
end
end

はい、承知いたしました。提供されたElixirのテスト実装方法について、具体的なサンプルケースを用いてさらに詳しく解説します。

Elixirのテストフレームワーク:ExUnit 🧪

Section titled “Elixirのテストフレームワーク:ExUnit 🧪”

Elixirの標準テストフレームワークはExUnitです。これは、シンプルで高速なテスト実行を可能にし、開発者がテストを簡単に書けるように設計されています。

  • use ExUnit.Case, async: true を使うと、テストを並行して実行できます。これにより、テストスイート全体の実行時間を大幅に短縮できます。

モック(Mocking)によるテスト 🎭

Section titled “モック(Mocking)によるテスト 🎭”

テスト対象のコードが外部サービスやデータベースに依存している場合、**モック(Mocking)**を使うことで、その依存関係を「偽物」に置き換え、テストを隔離して実行できます。これにより、テストが外部環境に影響されず、信頼性が高まります。

Elixirでは、Moxというライブラリがモックの標準的なツールとして広く使われています。

サンプルケース:ユーザー登録サービス 👥

Section titled “サンプルケース:ユーザー登録サービス 👥”

ユーザーを登録し、外部のメール配信サービスに通知を送るシンプルなサービスを例に見てみましょう。

  1. サービスの定義 (lib/auth/user_service.ex)

    • メールサービスを呼び出すロジックを含むサービスです。ここでは、Mailer というモジュールに依存しています。
    defmodule MyApp.Auth.UserService do
    # メールサービス(Mailer)に依存
    def create_user(user) do
    # ユーザーをデータベースに保存する処理(省略)
    IO.puts("User #{user.email} created.")
    # 外部サービス(Mailer)にメール送信を依頼
    case MyApp.Mailer.send_welcome_email(user) do
    :ok -> {:ok, user}
    {:error, reason} -> {:error, reason}
    end
    end
    end
  2. メールサービスインターフェース (lib/auth/mailer.ex)

    • テスト時にモックに置き換えられる「本物の」メールサービスです。ここでは、send_welcome_email/1という関数を定義します。
    defmodule MyApp.Mailer do
    def send_welcome_email(user) do
    # 実際には外部APIを呼び出す処理
    IO.puts("Sending welcome email to #{user.email}...")
    :ok
    end
    end
  3. テストの実装 (test/auth/user_service_test.exs)

    • このテストでは、MyApp.Auth.UserService.create_user/1のロジックをテストします。このとき、実際に外部のメールサービスを呼び出すと、テスト環境が汚染されたり、API制限にかかる可能性があります。そこで、Moxを使ってMyApp.Mailerをモックに置き換えます。
    defmodule MyApp.Auth.UserServiceTest do
    use ExUnit.Case, async: true
    import Mox # Moxライブラリをインポート
    # テスト終了時に、モックが期待通りに呼び出されたか検証する
    setup :verify_on_exit!
    test "ユーザーが作成されたら、ウェルカムメールが送信される" do
    # ユーザーデータを準備
    user = %{email: "test@example.com"}
    # Mockingの準備
    # MyApp.Mailerを、Mox.Stubという「偽物」のモジュールとして定義
    Mox.defmock(MyApp.Mailer, for: MyApp.Mailer)
    # 期待値の設定
    # MyApp.Mailerのsend_welcome_email/1関数が、
    # 引数として'user'を受け取ったら、`:ok`を返すことを期待する
    expect(MyApp.Mailer, :send_welcome_email, fn ^user -> :ok end)
    # テスト対象の関数を実行
    {:ok, _user} = MyApp.Auth.UserService.create_user(user)
    end
    end
    • Mox.defmock(MyApp.Mailer, for: MyApp.Mailer): MyApp.Mailerという「本物」のモジュールを、Moxが提供する「偽物」のモジュールに置き換えます。これにより、テスト実行中は、MyApp.Mailerへのすべての呼び出しがモックに転送されます。
    • expect(...): モックの振る舞いを定義します。この行は、「MyApp.Mailersend_welcome_email関数が、パターンマッチした引数(^user)で呼び出されたら、:okを返す」という期待を宣言しています。
    • setup :verify_on_exit!: この一行は魔法のようなものです。テストが終了したときに、expectで定義したすべての期待が満たされたかどうかを自動的に検証します。もしsend_welcome_emailが一度も呼ばれなかったり、違う引数で呼ばれたりした場合、テストは失敗します。

モックを使用することで、外部に依存することなく、サービスのビジネスロジック(この場合は、「ユーザー作成後にメールを送信する」という振る舞い)を正確かつ安全にテストすることができます。

Ectoとサンドボックスを使用したデータベーステスト

Section titled “Ectoとサンドボックスを使用したデータベーステスト”

Elixirでデータベースを扱う際は、Ectoライブラリが標準的に使用されます。データベースに依存するテストは、モックを使用せずに本物のデータベースを使い、テストごとにデータをクリーンアップするサンドボックスという方法が一般的です。

このアプローチは、実際にデータベースへの操作が行われるため、モックでは見つけられないような問題(制約違反など)を発見できるという利点があります。

サンプルケース:ブログ記事の投稿サービス 📝

Section titled “サンプルケース:ブログ記事の投稿サービス 📝”

ユーザーが新しいブログ記事を投稿する機能をテストする例を見てみましょう。この機能はデータベースへの書き込みを伴います。

  1. スキーマの定義 (lib/blog/post.ex)

    • ブログ記事のデータ構造を定義します。
    defmodule MyApp.Blog.Post do
    use Ecto.Schema
    schema "posts" do
    field :title, :string
    field :body, :string
    timestamps()
    end
    end
  2. リポジトリの定義 (lib/repo.ex)

    • データベースとの接続を管理します。
    defmodule MyApp.Repo do
    use Ecto.Repo, otp_app: :my_app
    end
  3. テストの実装 (test/blog/post_test.exs)

    • 新しい記事が正しくデータベースに保存されるかをテストします。
    defmodule MyApp.Blog.PostTest do
    use MyApp.DataCase # 後述するヘルパーモジュールをインポート
    test "新しい記事を正常に作成できる" do
    # 記事データを作成
    attrs = %{title: "My First Post", body: "Hello, World!"}
    # サービス関数を呼び出して記事を作成
    {:ok, post} = MyApp.Blog.create_post(attrs)
    # データベースに正しく保存されたことを確認
    assert post.title == "My First Post"
    assert post.body == "Hello, World!"
    # データベースから直接取得して検証
    retrieved_post = MyApp.Repo.get_by(MyApp.Blog.Post, title: "My First Post")
    assert retrieved_post.id == post.id
    end
    end
    • MyApp.DataCaseヘルパーモジュール: 上記のテストでは、use MyApp.DataCaseという行が重要です。これは、プロジェクトのテストヘルパーファイルで定義されたカスタムモジュールで、各テストをデータベースのトランザクション内に閉じ込める役割を担います。

    • ヘルパーモジュールの定義 (test/support/data_case.ex)

      • mix new--databaseオプションを付けてプロジェクトを作成すると、このファイルが自動で生成されます。
      defmodule MyApp.DataCase do
      use ExUnit.CaseTemplate
      using do
      quote do
      # Ectoのテストヘルパー関数をインポート
      use Ecto.Sandbox
      # データベース接続のヘルパーモジュールをインポート
      import MyApp.Repo
      end
      end
      setup do
      # 各テストが新しいトランザクションを開始するように設定
      :ok = Ecto.Sandbox.checkout(MyApp.Repo)
      # テスト後にトランザクションをロールバック(データを元に戻す)
      on_exit(fn -> Ecto.Sandbox.rollback(MyApp.Repo) end)
      :ok
      end
      end
    • テストデータの自動クリーンアップ: Ecto.Sandbox.checkoutEcto.Sandbox.rollbackにより、各テストが独立したトランザクション内で実行されます。テストが完了すると、そのトランザクションは自動的にロールバックされるため、テスト中に作成されたデータは次のテストに影響を与えません。これにより、テストごとにデータベースをリセットする手間が不要になります。

    • 実際のデータベースとのやり取り: モックを使用する代わりに、実際のEctoの機能やデータベースの振る舞いを検証できます。これは、データベースのスキーマや制約が原因で発生する可能性のあるバグを早期に発見する上で不可欠です。

この方法は、特に統合テストやデータベースに深く依存する機能のテストにおいて、信頼性と効率性の両方を兼ね備えたElixirの標準的なプラクティスです。

ジェネレーターとファクトリーを使用したテストデータ管理

Section titled “ジェネレーターとファクトリーを使用したテストデータ管理”

これまでの解説では、テストデータを手動で %{...} の形式で作成していました。しかし、大規模なアプリケーションでは、テストデータが複雑になり、手動での管理が困難になります。この問題を解決するために、ExMachinaのようなファクトリーライブラリが広く使われています。

ファクトリーは、テスト用のデータを簡単に生成するための機能で、**ジェネレーター(Generator)**と呼ばれる関数で、より高度なデータ生成ロジックをカプセル化できます。これにより、テストコードが簡潔になり、保守性が向上します。

サンプルケース:ユーザーと記事のテストデータ生成 👥📝

Section titled “サンプルケース:ユーザーと記事のテストデータ生成 👥📝”

ユーザーと記事という2つの関連モデルを持つアプリケーションを例に見てみましょう。

  1. ファクトリーの定義 (test/support/factories.ex)

    • ExMachina を使って、ユーザーと記事のファクトリーを定義します。
    defmodule MyApp.Factory do
    use ExMachina.Ecto, repo: MyApp.Repo
    # ユーザーファクトリー
    def user_factory do
    %{
    name: sequence(:name, &"user#{&1}"), # "user1", "user2"... と自動生成
    email: sequence(:email, &"user#{&1}@example.com")
    }
    end
    # 記事ファクトリー
    def post_factory do
    %{
    title: "My title",
    body: "My body",
    # 記事作成時に、関連するユーザーも自動で作成
    user: build(:user)
    }
    end
    end
  2. テストの実装 (test/blog/post_test.exs)

    • ファクトリーを使って、簡潔にテストデータを作成します。
    defmodule MyApp.Blog.PostTest do
    use MyApp.DataCase
    import MyApp.Factory # ファクトリーをインポート
    test "新しい記事を正常に作成できる" do
    # ファクトリーを使って記事データを作成
    # 関連するユーザーも自動的にデータベースに保存される
    post = insert(:post)
    # データベースから取得して検証
    retrieved_post = MyApp.Repo.get(MyApp.Blog.Post, post.id)
    # 検証
    assert retrieved_post.title == "My title"
    assert retrieved_post.body == "My body"
    assert is_integer(retrieved_post.user.id) # ユーザーが作成されたことを確認
    end
    test "タイトルを指定して記事を作成できる" do
    # 特定の値を上書きして記事を作成
    post = insert(:post, title: "Custom Title")
    # 検証
    assert post.title == "Custom Title"
    end
    end
    • insert(:post): ファクトリーで定義されたデフォルト値を使って新しい記事を作成し、データベースに保存します。この1行で、タイトルや本文、そして関連するユーザーまで自動的に生成・保存されるため、テストコードが非常に簡潔になります。

    • insert(:post, title: "Custom Title"): 特定のフィールドだけを上書きしたい場合も簡単です。デフォルトのtitleではなく、指定した"Custom Title"で記事を作成します。

    • ジェネレーター sequence: テスト実行ごとにユニークなデータ(例:user1@example.com, user2@example.com)を自動で生成できるため、テスト間のデータ競合を防ぎます。

ファクトリーを使用することで、手動でテストデータを作成・管理する手間がなくなり、テストコードがより読みやすく、再利用可能になります。これは、大規模なElixirアプリケーション開発において、テストの生産性を高めるための必須のプラクティスです。