こんにちは、フロントエンドエンジニアのやなぎ( @apple_yagi )です。
先日、バックエンドで行っているUnitテストの実装効率を上げるために簡単なSeeder(テストデータを作成するツール)を実装したのでご紹介します。
前提
PR TIMESのバックエンドはノンフレームワークのPHPで実装されています。また、ORM(Object-Relational Mapping)ツールなども使用しておらず、PDO と TetoSQL を利用して社内で開発した PrSQL という class を用いてデータベースを操作しています。
Unitテストは、PHPUnit を用いており、データベースをモックしない方針を取っています。
従来のテストデータの作成方法
これまでUnitテストで使用するテストデータを作成する際は、テストデータをinsertする用のSQLファイルを作成し、PDOのexecを使用してデータベースにSQLを実行していました。
INSERT INTO company ("id", "name", "zip", "address", "phone", "industry_type",
"description", "cover_image", "logo_image")
VALUES (1, "株式会社テスト", "000-0000", "東京都", "000-0000-0000",
"サービス業", "事業内容について", "/cover_image/test.png", "/logo_image/test.jpeg");
INSERT INTO press_release ("id", "company_id", "title", "sub_title", "body",
"thumbnail_image")
VALUES (1, 1, "タイトル", "サブタイトル", "本文", "/thumbnail_image/test.jpeg");<?php
trait CompanyRepoTestDataTrait
{
public function resetData(): void
{
// テスト用のデータベースと接続するPDOを取得
$pdo = PDOFactory::getPDOInstance();
// 上記のinit.sqlに記述されているSQLを取得
$sql_path = __DIR__ . '/init.sql';
$sql = file_get_contents($sql_path);
// SQLを実行
$pdo->exec($sql);
}
}
class CompanyRepoTest extends TestCase
{
use CompanyRepoTestDataTrait;
public function setUp(): void
{
parent::setUp();
$this->resetData();
}
public function testFindById(): void
{
$company = CompanyRepo::findById(1);
$this->assertInstanceOf(Company::class, $company);
}
}上記のような簡単なテストであれば、 init.sql に記述するSQLも少なく問題がないように見えます。
しかし、実際のUnitテストで用意するテストデータはもっと複雑なものが多く、色々なテーブルにデータをinsertする必要があります。また、 init.sql でinsertをしている company や press_release といったテーブルは、実際には100カラム以上あり、SQLで記述すると可読性が悪く、テストデータの生成するためのSQLの作成が難しい状況でした。
このような問題を解決するために、次に紹介するSeederを実装しました。
実装したSeederについて
PDOとPrSql、そして Faker を使用して、以下のように実装しました。
<?php
declare(strict_types=1);
namespace PRTIMES\PrTimes\Seeder;
use Faker\Factory;
use PDO;
use PRTIMES\PrSql;
class CompanySeeder
{
public static function seed(PDO $pdo, array $companyData, int $rowQuantity = 1): void
{
$faker = Factory::create("ja_JP");
for ($i = 0; $i < $rowQuantity; $i++) {
PrSql::execute($pdo, '
INSERT INTO company (
id,
name,
zip,
address,
phone,
industry_type,
description,
cover_image,
logo_image
) VALUES (
:id@int,
:name@string,
:zip@string,
:address@string,
:phone@string,
:industry_type@string,
:description@string,
:cover_image@string,
:logo_image@string
)
', [
':id' => $companyData['id'] ?? $faker->numberBetween(1, 999999999),
':name' => $companyData['name'] ?? $faker->company,
':zip' => $companyData['zip'] ?? $faker->postcode,
':address' => $companyData['address'] ?? $faker->address,
':phone' => $companyData['phone'] ?? $faker->phoneNumber,
':industry_type' => $companyData['industry_type'] ?? $faker->word,
':description' => $companyData['description'] ?? $faker->text,
':cover_image' => $companyData['cover_image'] ?? $faker->url,
':logo_image' => $companyData['logo_image'] ?? $faker->url
]);
}
}
}実装はとてもシンプルで、 $company_data という連想配列を受け取り、存在しないkey(カラム)の値は Faker で埋めるという実装です。また、 $rowQuantity の値を変えることで生成するデータの数を変更することができます。
このSeederを実装したことにより、テストデータの作成が簡単にできるようになりました。
<?php
class CompanyRepoTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
$pdo = PDOFactory::getPDOInstance();
CompanySeeder::seed($pdo, [
'id' => 1,
'name' => '株式会社Hoge'
]);
}
public function testFindById(): void
{
$company = CompanyRepo::findById(1);
$this->assertInstanceOf(Company::class, $company);
}
}しかし、この実装では100行以上のカラムが存在するテーブルのSeederを実装しようとした時に、コードの行数が長くなりすぎる(1つ1つカラムを埋める必要があるため)というデメリットがあります。
そのため、初めは以下のようなライブラリを利用することも検討していました。
しかし、phactoryはドキュメントがなく、GitHub にリンクされている http://phactory.org/ にもアクセスができず(2024年2月16日現在)、どのように使用するか探り探りでする必要があると感じたため、利用しませんでした。
また、db-seederはデータベースにデータをinsertする際に毎回 insert 先のテーブルに対して、 TRUNCATE を実行するような実装になっていました。PR TIMESではデータベースにPostgreSQLを使用しているのですが、TRUNCATE の SQL文に CASCADE オプションが付いておらず、外部キー制約がはられているテーブルでは TRUNCATE に失敗し、データの insert をすることができませんでした。
ForkしてPR TIMES用にカスタマイズすることも考えましたが、達成したいことに対して、too muchと感じたため、今回はライブラリを使用せずに、自前で実装することにしました。
まとめ
今回、素朴なSeederを実装することでテストの実装効率を上げました。
Seederを実装してから、これまではSQLを記述するのが大変で実装されていなかった、複雑なデータを用いたテストが実装されるようになりました。また、テストの実装スピードも向上したと感じています。
PR TIMESではこのような改善活動を1人1人が裁量を持っておこなっています。もしご興味ある方は、ぜひカジュアル面談でお話しできると嬉しいです。

