「GoQSystem Advent Calendar 2025」5日目
GoQSystemバックエンドエンジニアの松澤です。
「DIを使うとテストが書きやすくなる」この言葉を聞いたことがある方は多いと思います。
しかし、実際にコードでどう変わるのか、明確に説明できるでしょうか?
私自身、説明しようとすると言葉に詰まることがあったので、この機会に具体例を交えて整理してみました。
本記事で扱うこと
- DIあり・なしの実装パターンの比較
- テストがどのように楽になるかの具体例
本記事で扱わないこと
- DIの解説
- テスト以外のDIのメリット(保守性、拡張性など)
題材:商品の価格に割引を適用するクラス
「割引を計算するクラス」を依存として持つ、シンプルな例を題材にします。
src/app/
├── Domain/
│ └── Discounts/
│ ├── DiscountCalculatorInterface.php
│ └── DiscountCalculator.php
└── Services/
├── WithDI/
│ └── PriceService.php
└── WithoutDI/
└── PriceService.php
src/tests/
└── Feature/
└── Services/
├── WithDIPriceServiceTest.php
└── WithoutDIPriceServiceTest.php
DI を使わない場合
一見テストできていますが、問題があります。
本体コード
<?php namespace App\Services\WithoutDI; use App\Domain\Discounts\DiscountCalculator; class PriceService { public function getDiscountedPrice(int $price): int { $discountCalculator = new DiscountCalculator(); $discount = $discountCalculator->calculate(); return $price - $discount; } }
<?php namespace App\Domain\Discounts; class DiscountCalculator { public function calculate(): int { // 何らかの計算処理をして結果を返す return 100; } }
テストコード
<?php use App\Services\WithoutDI\PriceService; it('割引が適用される', function () { $service = new PriceService(); $result = $service->getDiscountedPrice(1000); expect($result)->toBe(900); });
何が問題なのでしょうか?
DiscountCalculatorを差し替えられないため、以下のようなテストが書けません
DIなしでは書けないテスト例
<?php it('割引が0円の場合', function () { // DiscountCalculatorは常に100を返すため、 // 0円のケースを再現できない }); it('割引計算で例外が発生した場合', function () { // DiscountCalculatorで例外を発生させる方法がない });
また、PriceServiceがDiscountCalculatorに強く依存しているため、DiscountCalculatorの仕様変更がPriceServiceのテスト全体に影響します。
DI を使う場合
本体コード
依存をコンストラクタで受け取ることで、テスト時にモックを渡せるようになります。
※Laravelではサービスコンテナが依存を自動解決するため、コントローラーなどで型宣言するだけで利用できます。
<?php namespace App\Services\WithDI; use App\Domain\Discounts\DiscountCalculatorInterface; class PriceService { public function __construct( private DiscountCalculatorInterface $calculator ) {} public function getDiscountedPrice(int $price): int { $discount = $this->calculator->calculate(); return $price - $discount; } }
<?php namespace App\Domain\Discounts; interface DiscountCalculatorInterface { public function calculate(): int; }
<?php namespace App\Domain\Discounts; class DiscountCalculator implements DiscountCalculatorInterface { public function calculate(): int { return 100; } }
テストコード
DIを使うと、依存クラスの挙動を自由にコントロールできるため、様々なケースをテストできます。 そのため、異常系のケースも簡単に書けるようになります。
モックを使って挙動を制御
<?php it('割引が適用される', function () { $calculator = Mockery::mock(DiscountCalculatorInterface::class); $calculator->shouldReceive('calculate') ->andReturn(100); $service = new PriceService($calculator); $result = $service->getDiscountedPrice(1000); expect($result)->toBe(900); });
モックを使って異常系テスト
<?php it('割引が0円の場合', function () { $calculator = Mockery::mock(DiscountCalculatorInterface::class); $calculator->shouldReceive('calculate') ->andReturn(0); $service = new PriceService($calculator); $result = $service->getDiscountedPrice(1000); expect($result)->toBe(1000); }); it('割引計算で例外が発生する', function () { $calculator = Mockery::mock(DiscountCalculatorInterface::class); $calculator->shouldReceive('calculate') ->andThrow(new Exception('計算失敗')); $service = new PriceService($calculator); $service->getDiscountedPrice(1000); })->throws(Exception::class, '計算失敗');
モックを使わずに正常系テストを書くことも選択できる
<?php it('割引が適用される', function () { // 実装クラスを直接注入してテスト $calculator = new DiscountCalculator(); $service = new PriceService($calculator); $result = $service->getDiscountedPrice(1000); expect($result)->toBe(900); });
このように、DIなしでは 再現できないテストが簡単に書けるようになります。
補足:なぜインターフェースを使うのか
インターフェースを使わない場合の問題
具象クラスをそのまま使うと、モックを注入する際に問題が発生することがあります。
例:DiscountCalculatorのコンストラクタが複雑な場合
<?php class DiscountCalculator { public function __construct( private Database $db, private CacheService $cache, private ConfigRepository $config ) {} public function calculate(): int { // DBやキャッシュを使った複雑な計算 return 100; } } class PriceService { public function __construct( private DiscountCalculator $calculator // 具象クラスを型宣言 ) {} }
テストでモックを渡そうとすると、コンストラクタの引数の扱いが難しくなる場合があります。
<?php // テストでモックを渡そうとすると... $mock = Mockery::mock(DiscountCalculator::class); // コンストラクタの引数(Database, CacheService, ConfigRepository)を // どう扱うかを考える必要があり、モック作成が複雑になる $mock->shouldReceive('calculate')->andReturn(100); $service = new PriceService($mock);
インターフェースを型宣言することで、実装の詳細(コンストラクタの依存関係など)に影響されず、モックも実装クラスも同じように扱えるようになり、テストが簡単に書けます。
まとめ
「DIを使うとテストが書きやすくなる」理由は、依存クラスの挙動を自由にコントロールできるようになるからです。 表にすると一目瞭然です。
| 観点 | DIなし | DIあり |
|---|---|---|
| 依存クラスの差し替え | できない | 可能 |
| 異常系テスト | できない | 可能 |
| 責務の明確さ | 低い | 高い |
| テストの安定性 | 他クラスの変更に左右される | 独立性が高い |
「テスタビリティの高いコードを書きなさい」と言われた際の参考になればと思います。
GoQSystemでは一緒に働くエンジニア/PdMを募集しています!!
GoQSystemの採用情報はこちら↓