Elixirテスト完全ガイド
Elixirテスト完全ガイド
Section titled “Elixirテスト完全ガイド”ElixirのテストフレームワークExUnitを使用したテスト実装を、実務で使える実装例とともに詳しく解説します。
1. ExUnitとは
Section titled “1. ExUnitとは”ExUnitの特徴
Section titled “ExUnitの特徴”ExUnitは、Elixirの標準テストフレームワークです。シンプルで高速なテスト実行を可能にします。
ExUnitの特徴 ├─ 並行実行(async: true) ├─ セットアップとクリーンアップ ├─ モックとスタブ └─ データベーステストなぜExUnitが必要か
Section titled “なぜExUnitが必要か”問題のある構成(テストなし):
# 問題: 手動での動作確認defmodule MyApp do def add(a, b) do a + b endend
# 手動でテスト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 endendはい、承知いたしました。提供されたElixirのテスト実装方法について、具体的なサンプルケースを用いてさらに詳しく解説します。
Elixirのテストフレームワーク:ExUnit 🧪
Section titled “Elixirのテストフレームワーク:ExUnit 🧪”Elixirの標準テストフレームワークはExUnitです。これは、シンプルで高速なテスト実行を可能にし、開発者がテストを簡単に書けるように設計されています。
use ExUnit.Case, async: trueを使うと、テストを並行して実行できます。これにより、テストスイート全体の実行時間を大幅に短縮できます。
モック(Mocking)によるテスト 🎭
Section titled “モック(Mocking)によるテスト 🎭”テスト対象のコードが外部サービスやデータベースに依存している場合、**モック(Mocking)**を使うことで、その依存関係を「偽物」に置き換え、テストを隔離して実行できます。これにより、テストが外部環境に影響されず、信頼性が高まります。
Elixirでは、Moxというライブラリがモックの標準的なツールとして広く使われています。
サンプルケース:ユーザー登録サービス 👥
Section titled “サンプルケース:ユーザー登録サービス 👥”ユーザーを登録し、外部のメール配信サービスに通知を送るシンプルなサービスを例に見てみましょう。
-
サービスの定義 (
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}endendend - メールサービスを呼び出すロジックを含むサービスです。ここでは、
-
メールサービスインターフェース (
lib/auth/mailer.ex)- テスト時にモックに置き換えられる「本物の」メールサービスです。ここでは、
send_welcome_email/1という関数を定義します。
defmodule MyApp.Mailer dodef send_welcome_email(user) do# 実際には外部APIを呼び出す処理IO.puts("Sending welcome email to #{user.email}..."):okendend - テスト時にモックに置き換えられる「本物の」メールサービスです。ここでは、
-
テストの実装 (
test/auth/user_service_test.exs)- このテストでは、
MyApp.Auth.UserService.create_user/1のロジックをテストします。このとき、実際に外部のメールサービスを呼び出すと、テスト環境が汚染されたり、API制限にかかる可能性があります。そこで、Moxを使ってMyApp.Mailerをモックに置き換えます。
defmodule MyApp.Auth.UserServiceTest douse ExUnit.Case, async: trueimport 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)endendMox.defmock(MyApp.Mailer, for: MyApp.Mailer):MyApp.Mailerという「本物」のモジュールを、Moxが提供する「偽物」のモジュールに置き換えます。これにより、テスト実行中は、MyApp.Mailerへのすべての呼び出しがモックに転送されます。expect(...): モックの振る舞いを定義します。この行は、「MyApp.Mailerのsend_welcome_email関数が、パターンマッチした引数(^user)で呼び出されたら、:okを返す」という期待を宣言しています。setup :verify_on_exit!: この一行は魔法のようなものです。テストが終了したときに、expectで定義したすべての期待が満たされたかどうかを自動的に検証します。もしsend_welcome_emailが一度も呼ばれなかったり、違う引数で呼ばれたりした場合、テストは失敗します。
- このテストでは、
モックを使用することで、外部に依存することなく、サービスのビジネスロジック(この場合は、「ユーザー作成後にメールを送信する」という振る舞い)を正確かつ安全にテストすることができます。
Ectoとサンドボックスを使用したデータベーステスト
Section titled “Ectoとサンドボックスを使用したデータベーステスト”Elixirでデータベースを扱う際は、Ectoライブラリが標準的に使用されます。データベースに依存するテストは、モックを使用せずに本物のデータベースを使い、テストごとにデータをクリーンアップするサンドボックスという方法が一般的です。
このアプローチは、実際にデータベースへの操作が行われるため、モックでは見つけられないような問題(制約違反など)を発見できるという利点があります。
サンプルケース:ブログ記事の投稿サービス 📝
Section titled “サンプルケース:ブログ記事の投稿サービス 📝”ユーザーが新しいブログ記事を投稿する機能をテストする例を見てみましょう。この機能はデータベースへの書き込みを伴います。
-
スキーマの定義 (
lib/blog/post.ex)- ブログ記事のデータ構造を定義します。
defmodule MyApp.Blog.Post douse Ecto.Schemaschema "posts" dofield :title, :stringfield :body, :stringtimestamps()endend -
リポジトリの定義 (
lib/repo.ex)- データベースとの接続を管理します。
defmodule MyApp.Repo douse Ecto.Repo, otp_app: :my_append -
テストの実装 (
test/blog/post_test.exs)- 新しい記事が正しくデータベースに保存されるかをテストします。
defmodule MyApp.Blog.PostTest douse 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.idendend-
MyApp.DataCaseヘルパーモジュール: 上記のテストでは、use MyApp.DataCaseという行が重要です。これは、プロジェクトのテストヘルパーファイルで定義されたカスタムモジュールで、各テストをデータベースのトランザクション内に閉じ込める役割を担います。 -
ヘルパーモジュールの定義 (
test/support/data_case.ex)mix newで--databaseオプションを付けてプロジェクトを作成すると、このファイルが自動で生成されます。
defmodule MyApp.DataCase douse ExUnit.CaseTemplateusing doquote do# Ectoのテストヘルパー関数をインポートuse Ecto.Sandbox# データベース接続のヘルパーモジュールをインポートimport MyApp.Repoendendsetup do# 各テストが新しいトランザクションを開始するように設定:ok = Ecto.Sandbox.checkout(MyApp.Repo)# テスト後にトランザクションをロールバック(データを元に戻す)on_exit(fn -> Ecto.Sandbox.rollback(MyApp.Repo) end):okendend -
テストデータの自動クリーンアップ:
Ecto.Sandbox.checkoutとEcto.Sandbox.rollbackにより、各テストが独立したトランザクション内で実行されます。テストが完了すると、そのトランザクションは自動的にロールバックされるため、テスト中に作成されたデータは次のテストに影響を与えません。これにより、テストごとにデータベースをリセットする手間が不要になります。 -
実際のデータベースとのやり取り: モックを使用する代わりに、実際の
Ectoの機能やデータベースの振る舞いを検証できます。これは、データベースのスキーマや制約が原因で発生する可能性のあるバグを早期に発見する上で不可欠です。
この方法は、特に統合テストやデータベースに深く依存する機能のテストにおいて、信頼性と効率性の両方を兼ね備えたElixirの標準的なプラクティスです。
ジェネレーターとファクトリーを使用したテストデータ管理
Section titled “ジェネレーターとファクトリーを使用したテストデータ管理”これまでの解説では、テストデータを手動で %{...} の形式で作成していました。しかし、大規模なアプリケーションでは、テストデータが複雑になり、手動での管理が困難になります。この問題を解決するために、ExMachinaのようなファクトリーライブラリが広く使われています。
ファクトリーは、テスト用のデータを簡単に生成するための機能で、**ジェネレーター(Generator)**と呼ばれる関数で、より高度なデータ生成ロジックをカプセル化できます。これにより、テストコードが簡潔になり、保守性が向上します。
サンプルケース:ユーザーと記事のテストデータ生成 👥📝
Section titled “サンプルケース:ユーザーと記事のテストデータ生成 👥📝”ユーザーと記事という2つの関連モデルを持つアプリケーションを例に見てみましょう。
-
ファクトリーの定義 (
test/support/factories.ex)ExMachinaを使って、ユーザーと記事のファクトリーを定義します。
defmodule MyApp.Factory douse 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)}endend -
テストの実装 (
test/blog/post_test.exs)- ファクトリーを使って、簡潔にテストデータを作成します。
defmodule MyApp.Blog.PostTest douse MyApp.DataCaseimport 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) # ユーザーが作成されたことを確認endtest "タイトルを指定して記事を作成できる" do# 特定の値を上書きして記事を作成post = insert(:post, title: "Custom Title")# 検証assert post.title == "Custom Title"endend-
insert(:post): ファクトリーで定義されたデフォルト値を使って新しい記事を作成し、データベースに保存します。この1行で、タイトルや本文、そして関連するユーザーまで自動的に生成・保存されるため、テストコードが非常に簡潔になります。 -
insert(:post, title: "Custom Title"): 特定のフィールドだけを上書きしたい場合も簡単です。デフォルトのtitleではなく、指定した"Custom Title"で記事を作成します。 -
ジェネレーター
sequence: テスト実行ごとにユニークなデータ(例:user1@example.com,user2@example.com)を自動で生成できるため、テスト間のデータ競合を防ぎます。
ファクトリーを使用することで、手動でテストデータを作成・管理する手間がなくなり、テストコードがより読みやすく、再利用可能になります。これは、大規模なElixirアプリケーション開発において、テストの生産性を高めるための必須のプラクティスです。