※本記事は「RailsからLaravelを眺める」シリーズの第5回です。Rails出身の私がLaravelを触りながら、Railsと比較して違いを整理していく連載になります。
はじめに
RailsからLaravelに移ったとき、まず目についたのがテストの文化の違いでした。RailsではRSpecが事実上の標準であり、テストコードは仕様を記述する手段として扱われます。DSLを駆使して「テストがそのままドキュメントになる」ことを目指す雰囲気が強いのに対し、LaravelではPHPUnitが標準であり、クラシカルで堅実なスタイルが根付いています。両者は同じ「テストを書く」という行為を扱っているにもかかわらず、その思想やアプローチには明確な違いが存在します。
この記事では、RSpecとPHPUnitを実際のコード例を交えて比較し、RailsとLaravelのテスト文化の差を解説します。さらに近年Laravel界隈で注目されているPestについても触れ、最後にMockやDependency Injection(DI)の違いから、両フレームワークの設計思想の対比を浮かび上がらせます。
歴史的背景
RSpecは2005年に誕生し、BDD(Behavior Driven Development) の思想をRubyに持ち込みました。Railsと組み合わさることで「テストは設計の一部である」という文化を形成し、Minitestが標準で付属していながらも、実務の現場では圧倒的にRSpecが選ばれてきました。
一方でPHPUnitは、JUnitの流れを汲みつつ2004年頃から進化してきた歴史を持ちます。PHPの世界全体で広く使われることを目指しており、Laravelは初期からこの実績あるフレームワークを採用しました。つまりLaravelのテスト文化は「PHP全体の伝統」を継承したものでもあります。
そして近年、PHPUnitの表現力に物足りなさを感じたコミュニティから「もっとシンプルに、RSpecライクに書きたい」という声が高まり、2020年にPestが登場しました。これによりLaravelのテスト文化も新たな進化を遂げようとしています。
RSpec:Railsの文化を体現するフレームワーク
RSpecは自然言語的なDSLを通じて、テストを仕様書のように読める形にします。Rails開発者は「テストが書けるかどうか」を設計の良し悪しを判断する基準にすることも多く、その背景にはRSpecの存在があります。
コード例
RSpec.describe User, type: :model do
context "with valid attributes" do
it "is valid with a name and email" do
user = User.new(name: "Alice", email: "alice@example.com")
expect(user).to be_valid
end
end
context "with invalid attributes" do
it "is invalid without an email" do
user = User.new(name: "Alice")
expect(user).to_not be_valid
end
end
end
この例では「名前とメールがあれば有効」「メールがなければ無効」という仕様がそのまま伝わります。RSpecはこうした直感的な表現力によって、仕様とテストを一体化させます。
PHPUnit:Laravelの実用的なアプローチ
LaravelではPHPUnitが標準です。クラスベースのスタイルはシンプルで、静的解析やIDEサポートと相性が良いという利点があります。LaravelはさらにHTTPリクエストやデータベース操作をテストするためのヘルパーを提供し、開発者が日常的に必要とする「実際の挙動の確認」を容易にします。
コード例
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Models\User;
class UserTest extends TestCase
{
public function test_valid_user()
{
$user = new User(['name' => 'Alice', 'email' => 'alice@example.com']);
$this->assertEquals('Alice', $user->name);
}
public function test_invalid_user_without_email()
{
$this->expectException(\Illuminate\Database\QueryException::class);
User::create(['name' => 'Alice']);
}
}
RSpecと比べると抽象度は低いですが、Laravelに備わっている assertDatabaseHas
や $this->post()
などを組み合わせると、現実的な業務ロジックに即したテストを書くことができます。
Pest:RSpecライクな記法をPHPにもたらす試み
PestはPHPUnit互換でありながら、記法はより簡潔で宣言的です。RSpecやJestを使ったことがある人にとってはすぐに馴染めるでしょう。
コード例
<?php
use App\Models\User;
it('creates a valid user', function () {
$user = User::factory()->make(['name' => 'Alice']);
expect($user->name)->toBe('Alice');
});
it('fails without an email', function () {
$this->expectException(\Illuminate\Database\QueryException::class);
User::create(['name' => 'Alice']);
});
実務ではまだ導入例は少ないですが、Laravelの開発者にとってRSpec的な快適さを補う有力な選択肢になりつつあります。
MockとDIの違いが示す文化の差
RSpecとPHPUnitを比較する上で欠かせないのがMockとDIの扱いです。Railsは依存注入を強調しない設計思想を持ち、RSpecのモック機能がその不足を補ってきました。テストコード内でdouble
やallow
を用い、依存を直接差し替えるスタイルが一般的です。
RSpecでのMock例
RSpec.describe PaymentService do
it "calls the gateway with correct amount" do
gateway = instance_double("Gateway")
allow(gateway).to receive(:charge).and_return(true)
service = PaymentService.new(gateway)
result = service.charge(1000)
expect(result).to be_truthy
expect(gateway).to have_received(:charge).with(1000)
end
end
RailsのDI文化が薄い分、RSpecが強力に依存の差し替えを支えてきたと言えます。
一方LaravelはDIコンテナを前提に設計されており、依存は常にコンストラクタやサービスコンテナを通じて解決されます。テストではMockeryなどを使って依存をモック化し、コンテナに登録して差し替える流れが自然です。
LaravelでのMock例
<?php
use Tests\TestCase;
use App\Services\PaymentService;
use App\Contracts\Gateway;
use Mockery;
class PaymentServiceTest extends TestCase
{
public function test_charge_with_mocked_gateway()
{
$mock = Mockery::mock(Gateway::class);
$mock->shouldReceive('charge')
->once()
->with(1000)
->andReturn(true);
// コンテナにモックをバインド
$this->app->instance(Gateway::class, $mock);
$service = app(PaymentService::class);
$result = $service->charge(1000);
$this->assertTrue($result);
}
}
このようにDIコンテナを活用することで、本番環境と同じ依存解決の流れを保ちながら、テストではモックを差し込めます。
実務でのテスト戦略の違い
RailsとLaravelでは「どのレイヤーでどのくらいテストを書くか」という考え方にも違いがあります。RailsはRSpec文化の影響もあり、ユニットテストを細かく積み上げ、さらにリクエストスペックやシステムスペックで全体の挙動を保証するという多層構造を重視します。モデルのバリデーションやコールバック、サービスオブジェクトのメソッド一つひとつに対してテストを書くことも珍しくなく、テストコード自体が設計の精密な写し鏡になります。
Laravelの場合は少し傾向が違い、ユニットテストよりもFeatureテストに比重が置かれることが多いです。たとえばコントローラやルーティングの動作を $this->get('/users')
や $this->post('/login')
でシミュレーションし、レスポンスのステータスやビューに含まれる文字列を確認するといったテストを重点的に書くスタイルです。これはLaravelが「HTTPを中心にしたフレームワーク」であることと関係しており、アプリケーションの主要なユースケースを「実際にリクエストを投げる形」で検証するのが自然だからです。
E2Eにあたるブラウザテストも両者で文化が違います。RailsはCapybaraを使い、ブラウザをシミュレートしてリンククリックやフォーム入力を確認します。LaravelはDuskを用意しており、実際にChromeドライバーを動かしてブラウザ操作を自動化できます。RailsのCapybaraが「RSpecの一部としてのDSL」で完結しているのに対し、LaravelのDuskは「別プロセスのブラウザを立ち上げる」というより実務寄りのスタイルで、非同期処理やVue/Reactなどフロントエンドとの統合にも強いのが特徴です。
ハマりどころとTips
実際に両方を使ってみると、テスト環境特有のハマりどころが見えてきます。Railsの場合、FactoryBotの create
と build
の違いを意識しないと不要にDBを叩き、テストが極端に遅くなることがあります。またCapybaraのテストは非同期処理の待ちに弱く、明示的に have_selector
で待たないとテストが不安定になります。RSpecはDSLが強力である一方、柔軟すぎてbeforeフックやletを多用しすぎると可読性を失うという落とし穴もあります。
LaravelではRefreshDatabase
トレイトが便利ですが、大規模マイグレーションがあると毎回リセットで時間がかかる問題があります。またMockeryを利用する際はテスト終了後に Mockery::close()
を呼ばないとリークする問題があり、CI環境ではここで失敗することもあります。Laravel特有の便利なアサーション(assertDatabaseHas
など)は非常に強力ですが、内部実装を知っていないと「false positive」を生みやすい点にも注意が必要です。
両者に共通する落とし穴もあります。テストが積み重なると実行時間が長くなり、ローカルでの開発体験を阻害することです。RSpecでもLaravelでも並列実行の仕組みが整っているので、ある程度の規模になったらCIでの並列化を真剣に検討する必要があります。
まとめ
RailsとLaravelのテスト文化を比較すると、それぞれの思想が色濃く反映されていることがわかります。RSpecはBDDの流れを汲み、テストを仕様記述として捉え、コードそのものに設計思想を刻み込みます。テストが書けなければ設計が悪い、という考え方すら存在し、テストはプロジェクトの「生きた仕様書」として機能します。
Laravelはより実用的で、フレームワークに標準で備わっているDIコンテナやHTTPヘルパーを活用し、アプリケーションの主要な動作をシンプルに検証します。ユニットテストよりもFeatureテストに重きを置く文化は、HTTPを中心としたフレームワークであるLaravelにとって合理的な選択です。MockeryとDIコンテナを組み合わせることで、依存を簡単に差し替えられる点も「実用性重視」の哲学を象徴しています。
そしてPestの登場により、Laravelの世界にもRSpec的な表現力を求める風潮が広がっています。Rails出身者にとっては、まずはPHPUnitとLaravelの標準的な書き方に慣れ、必要に応じてPestを導入するのがよいでしょう。RSpecの心地よさを求めつつ、Laravelならではの便利さも享受する。この両立こそが、RailsからLaravelに移行する際の最適解だと思います。
コメント