質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Model

MVCモデルの一部であるModelはアプリケーションで扱うデータとその動作を管理するために扱います。

リファクタリング

リファクタリングとはコードの本体を再構築するための手法であり、外見を変更せずに内部構造を変更/改善させることを指します。

テスト駆動開発

テスト駆動開発は、 プログラム開発手法の一種で、 プログラムに必要な各機能をテストとして書き、 そのテストが動作する必要最低限な実装を行い コードを洗練させる、といったサイクルを繰り返す手法の事です。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

Q&A

解決済

3回答

1473閲覧

DBにアクセスしないユニットテストのコーディングをしたい

donut4

総合スコア170

Model

MVCモデルの一部であるModelはアプリケーションで扱うデータとその動作を管理するために扱います。

リファクタリング

リファクタリングとはコードの本体を再構築するための手法であり、外見を変更せずに内部構造を変更/改善させることを指します。

テスト駆動開発

テスト駆動開発は、 プログラム開発手法の一種で、 プログラムに必要な各機能をテストとして書き、 そのテストが動作する必要最低限な実装を行い コードを洗練させる、といったサイクルを繰り返す手法の事です。

Laravel 5

Laravel 5は、PHPフレームワークLaravelの最新バージョンで、2014年11月に発表予定です。ディレクトリ構造がが現行版より大幅に変更されるほか、メソッドインジェクションやFormRequestの利用が可能になります。

0グッド

0クリップ

投稿2021/06/03 11:07

編集2021/06/07 06:51

Laravel単体テストでDBにアクセスしないでユニットテストをコーディングしたいと思ってます。

ドリンクのECサイト的なWEBアプリを作ってます。
飲み物購入時に行われるDBアクセス(ストックを確認する、未成年のチェック、残金を減らしてストック減らすなど)を行うコントローラのユニットテストを書くことになりました。

書くことになったのですが、、、
後述のDrinkService.phpはDBへの永続化を責務とするDrinkRepository.php(DBモデルクラス)に直接依存しているため、、、

DrinkServiceクラス, DrinkRepositoryクラスをリファクタリングし
DBに依存しないユニットテストを書く必要がある(テストごとにDBの状態を適切にセットする)とのことなのですが、、、

このような設計センス(?)を今まで意識してコーディングする機会がなかったので、いきなり言われて戸惑ってます。

どのようなコーディングが「DBに直接依存している」ことを指すのか、
また直接依存してたらなぜよろしくないのか?リファクタリングしたらどのようないい事があるのかなど教えていただけたらと思います。(新たなクラス, 抽象クラス, インターフェースを実装はあり)

よろしくお願いします。

##追記
DBに直接依存しているとはdbにアクセスが行われることとご回答をいただきました。

そこで今度は、DBにアクセスさせないで上記クラスをリファクタリングしたり、別クラスを追加したりして実現できるものなのか?とまた疑問に感じました?(予め擬似的なDBで使われる値をセットした別クラスを用意しておくとか?)

##ソース(計3ファイル)

controler

1<?php 2 3declare(strict_types=1); 4 5namespace App\Services\Drink; 6 7use App\Exceptions\Drink\Buy\GuiltyException; 8use App\Exceptions\Drink\Buy\NoStockException; 9use App\Exceptions\Drink\Buy\NotEnoughMoneyException; 10use App\Exceptions\Drink\NotFoundException; 11use App\Models\Drink; 12use App\Models\User; 13use App\Repositories\DrinkRepository; 14use App\Repositories\UserRepository; 15 16/** 17 * Class DrinkService 18 * @package App\Services\Drink 19 */ 20final class DrinkService 21{ 22 private DrinkRepository $drinkRepository; 23 private UserRepository $userRepository; 24 25 public function __construct() 26 { 27 $this->drinkRepository = new DrinkRepository(); 28 $this->userRepository = new UserRepository(); 29 } 30 31 /** 32 * ドリンク一覧を取得する 33 * @return iterable 34 */ 35 public function list(): iterable 36 { 37 return $this->drinkRepository->all(); 38 } 39 40 /** 41 * ドリンクを購入する 42 * @param User $user 購入するユーザー 43 * @param int $drinkId ドリンクID 44 * @return DrinkBoughtResult 購入後のドリンクとユーザーの状態 45 * @throws GuiltyException 46 * @throws NoStockException 47 * @throws NotEnoughMoneyException 48 * @throws NotFoundException|\App\Exceptions\User\NotFoundException 49 */ 50 public function buy(User $user, int $drinkId): DrinkBoughtResult 51 { 52 $drink = $this->drinkRepository->find($drinkId); 53 54 if (is_null($drink)) { 55 throw new NotFoundException(); 56 } 57 58 if ($drink->stock <= 0) { 59 throw new NoStockException(); 60 } 61 62 if ($user->wallet < $drink->price) { 63 throw new NotEnoughMoneyException(); 64 } 65 66 if ($drink->isAlcohol && $user->age < 20) { 67 throw new GuiltyException(); 68 } 69 70 $newDrink = new Drink( 71 $drink->id, 72 $drink->name, 73 $drink->price, 74 $drink->stock - 1, 75 $drink->isAlcohol, 76 ); 77 78 $newUser = new User( 79 $user->id, 80 $user->name, 81 $user->age, 82 $user->wallet - $drink->price, 83 ); 84 85 $this->drinkRepository->save($newDrink); 86 $this->userRepository->save($newUser); 87 88 return new DrinkBoughtResult($newUser, $newDrink); 89 } 90} 91

Model

1<?php 2 3declare(strict_types=1); 4 5namespace App\Repositories; 6 7use App\Exceptions\Drink\NotFoundException; 8use App\Models\Drink; 9use Illuminate\Database\ConnectionInterface; 10 11/** 12 * Class DrinkRepository 13 * @package App\Repositories 14 */ 15final class DrinkRepository 16{ 17 /** 18 * テーブル名 19 */ 20 private const TABLE = 'drinks'; 21 22 /** 23 * @var ConnectionInterface 24 */ 25 private ConnectionInterface $connection; 26 27 public function __construct() 28 { 29 $this->connection = app()->get(ConnectionInterface::class); 30 } 31 32 /** 33 * ドリンクを全件取得する 34 * @return iterable 35 */ 36 public function all(): iterable 37 { 38 return $this->connection->table(self::TABLE) 39 ->get() 40 ->map(fn(\stdClass $d) => $this->createDrinkModel($d)); 41 } 42 43 /** 44 * ドリンクをIDにより取得する 45 * @param int $id 46 * @return Drink|null 47 */ 48 public function find(int $id): ?Drink 49 { 50 $d = $this->connection->table(self::TABLE) 51 ->find($id); 52 53 return is_null($d) ? null : $this->createDrinkModel($d); 54 } 55 56 /** 57 * ドリンクを保存する 58 * @param Drink $drink 59 * @throws NotFoundException 60 */ 61 public function save(Drink $drink): void 62 { 63 $query = $this->connection->table(self::TABLE) 64 ->where('id', $drink->id); 65 if (!$query->exists()) { 66 throw new NotFoundException('ドリンクの新規作成はできません。'); 67 } 68 69 $query->update( 70 [ 71 'name' => $drink->name, 72 'price' => $drink->price, 73 'stock' => $drink->stock, 74 'is_alcohol' => $drink->isAlcohol, 75 ] 76 ); 77 } 78 79 private function createDrinkModel(\stdClass $d): Drink 80 { 81 return new Drink( 82 $d->id, 83 $d->name, 84 $d->price, 85 $d->stock, 86 (boolean)$d->is_alcohol, 87 ); 88 } 89} 90

TestCode

1<?php 2 3declare(strict_types=1); 4 5namespace Tests\Unit\Services\Drink; 6 7use App\Exceptions\Drink\Buy\NoStockException; 8use App\Models\User; 9use App\Services\Drink\DrinkService; 10use Tests\TestCase; 11 12class DrinkServiceBuyTest extends TestCase 13{ 14 // TODO: DrinkService::buy()の分岐をすべてテストすること 15 16 /** 17 * FIXME: 在庫数がDBに依存しており, DBの状態によってこのテストは失敗するので困っています。 18 * @testdox 在庫が0以下の場合, NoStockExceptionをスローすること 19 * @throws 20 */ 21 public function test_buy_should_throws_no_stock_exception_if_stock_0_or_less(): void 22 { 23 $this->expectException(NoStockException::class); 24 25 $service = new DrinkService(); 26 $user = new User(1, 'テストユーザー', 20, 100); 27 $service->buy($user, 1); 28 } 29} 30

##DB定義

create table drinks ( id bigint unsigned auto_increment comment 'ID' primary key, name varchar(255) not null comment '商品名', price int unsigned not null comment '価格', stock int unsigned not null comment '在庫数', is_alcohol tinyint(1) not null comment 'アルコール飲料であるか' ) collate=utf8mb4_unicode_ci; create table users ( id bigint unsigned auto_increment comment 'ID' primary key, name varchar(255) not null comment '名前', age tinyint unsigned not null comment '年齢', wallet int unsigned not null comment '所持金' )

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

Laravel でのクリーンアーキテクチャとテストをどうすべきかについての持論です

既存の実装のダメ出しをすると,極端な話にはなりますが

php

1/** 2 * ドリンクをIDにより取得する 3 * @param int $id 4 * @return Drink|null 5 */ 6public function find(int $id): ?Drink 7{ 8 $d = $this->connection->table(self::TABLE) 9 ->find($id); 10 11 return is_null($d) ? null : $this->createDrinkModel($d); 12}

このメソッドで find() 結果を返してる時点でリポジトリ失格です。なぜなら返り値を使って ->fill(...)->save(); とか勝手に呼べちゃうからです。 それをやってはいけないと縛るのは,プロジェクトをメンテナンスする人の責任であり,チームメンバーへの思想の共有がスムーズにできない or 怠るのであればやってはなりません。

Laravel が ActiveRecord ベースのフレームワークなので Repository パターンは基本的にミスマッチだと思っていますが,それでもどうしてもやりたいなら

  • Eloquent Model は返さず,データベースに一切依存しない,値を詰め込むだけのオブジェクトを1個1個丁寧に定義する
  • Eloquent Model からデータベースコネクションを削除する( Model クラスの定義そのままでは実現できないので改造必須)

のいずれかぐらいはやっておいたほうがいいかなと思います。

投稿2021/07/20 14:41

mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

自己解決

DBにアクセスしないではなくDBに依存しないでした。
テストデータを挿入して実行する実用があるみたいです。

投稿2021/06/07 06:52

donut4

総合スコア170

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

ユニットテストはリポジトリのチェックアウト直後に何も設定しなくてもテストを実行して通る状態が良いです。
そのためDBに依存してはいけません。
「DBに依存している」とは、テスト実行時にDBアクセスが行われる状態のことです。
また、DBに依存していると、自分以外の誰かが自分の参照しているDBと違うDBのデータでテストが通るようにしてコミットすると、自分の環境ではコードは正しく、何も間違ったことをしていなくてもテストが通らなくなります。

投稿2021/06/03 22:38

rysh

総合スコア874

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

donut4

2021/06/04 07:58

ありがとうございます。 DBにアクセスしないとのことですが、DrinkRepositoryのアクセスをしないで、どうやってSQLが正しく機能しているかのテストをするのでしょうか? 上記クラスをリファクタリングしたり、別クラスを追加したりしてどうにかなるものなのでしょうか?
rysh

2021/06/04 08:04

PHPは勝手がわからないんですが、SQLをテストしないとバグるんですか? どうしてもテストしたい時はSQLを出力して文字列としてアサーションするか、H2のようなオンメモリDBを使うと良いと思いますよ。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問