@suin さんにコメントを頂いています。
大変参考になりますので是非コメントも合わせて読んでください。
https://qiita.com/tkek321/items/5803e7affc256de7a735#comments
DB接続やファイルの削除(unlink)をしているクラスにテストを書きたかった
アップロードしたファイルをディレクトリに保存し、
ファイル名をDBに保存してあるシステムがあるとします。
DBからファイル名を削除すると、ディレクトリの中からもファイルが消えて欲しい。
DBからファイル名を配列で取得し、unlink関数を利用してディレクトリ内のファイルを削除しているクラス
DB: table
id | filename |
---|---|
1 | hogehoge.img |
2 | foobar.img |
ディレクトリ構成
src/
img/
hogehoge.img
foobar.img
FileRemover.php
FilesRemover.php
<?php
/**
* 現在使用している画像ファイルがDBに登録されている。
* DBからファイル名を取得して使用していないファイルを削除する。
*/
class FilesRemover
{
private const DIR_PATH = __DIR__ . '/img';
private $pdo;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function remove(): void
{
$this->removeFiles($this->getNotRemoveFilePaths());
}
/**
* 配列に含まれていないファイルを削除する
*/
private function removeFiles(array $array): void
{
$filePaths = glob($this->dirPath.'/*');
foreach ($filePaths as $filePath) {
$fileName = basename($filePath);
if (in_array($fileName, $notRemoveFiles, true) === false) {
unlink(self::DIR_PATH . '/' . $fileName);
}
}
}
/**
* DBから使用されているファイル名を取得
*/
private function getNotRemoveFilePaths(): array
{
$stmt = $this->pdo->query('SELECT file_name FROM table');
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$fileNames = [];
foreach ($results as $result) {
$fileNames[] = $result['file_name'];
}
return $fileNames;
}
}
このままではテストしづらいのでinterfaceを利用してテストコードを書けるようにしていきたい。
インターフェースに依存するコードに書き直す
まずはテストが書きづらい、DBに接続しているコードとunlink関数を使用しているコードをインターフェースに依存する形に書き直します。
Filesystem.php
<?php
namespace App;
interface Filesystem
{
public function remove(string $fileName): void;
public function getFileNames(): array;
}
FilesRemover.php
<?php
namespace App;
class FilesRemover
{
private $filesystem;
private $dirPath;
/**
* Filesystemインターフェースを外部から注入
* Filesystemの関数を利用してファイル名の取得、削除を行う
*/
public function __construct(Filesystem $filesystem, string $dirPath = __DIR__ . '/img')
{
$this->filesystem = $filesystem;
$this->dirPath = $dirPath;
}
public function remove(): void
{
$this->removeFiles($this->filesystem->getFileNames());
}
private function removeFiles(array $notRemoveFiles): void
{
$filePaths = glob($this->dirPath.'/*');
foreach ($filePaths as $filePath) {
$fileName = basename($filePath);
if (in_array($fileName, $notRemoveFiles, true) === false) {
$this->filesystem->remove($this->dirPath . '/' . $fileName);
}
}
}
}
RealFilesystem
<?php
namespace App;
class RealFilesystem implements Filesystem
{
private $pdo;
public function __construct(\PDO $pdo)
{
$this->pdo = $pdo;
}
public function remove(string $fileName): void
{
unlink($fileName);
}
public function getFileNames(): array
{
$stmt = $this->pdo->query('SELECT file_name FROM foo');
$results = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$fileNames = [];
foreach ($results as $result) {
$fileNames[] = $result['file_name'];
}
return $fileNames;
}
}
インターフェースを利用してテストを書く(インターフェースを継承したMockクラスを作成する)
ディレクトリ構成
src/
img/
FileRemover.php
Filesystem.php
RealFilesystem.php
tests/
mock-img/
mock-file-a.txt
mock-file-b.txt
mock-file-del-a.txt
mock-file-del-b.txt
FileRemoveTest.php
MockFilesystem.php
テスト用にmock-img
ディレクトリとファイルを作成する。
MockFilesystem.php
<?php
use App\Filesystem;
/**
* テスト用のMockクラス
*/
class MockFilesystem implements Filesystem
{
private $removedFiles = [];
/**
* ファイルの削除ではなく実際のコードではunlink関数に渡されるファイル名を
* 配列として保存している。
* つまり、配列に保存されるファイル名が本来であれば削除されるファイル名。
*/
public function remove(string $name): void
{
$this->removedFiles[] = $name;
}
/**
* 単純に削除しない予定のファイル名の配列を返すだけの関数。
*/
public function getFileNames(): array
{
return [
'mock-file-a.txt',
'mock-file-b.txt',
];
}
/**
* 保存したファイル名の配列を取得するための関数。
*/
public function getRemoveFiles()
{
return $this->removedFiles;
}
}
FilesRemoverTest
<?php
require_once __DIR__.'/../vendor/autoload.php';
require_once __DIR__.'/MockFilesystem.php';
use PHPUnit\Framework\TestCase;
class FilesRemoverTest extends TestCase
{
/**
* @test
*/
public function 配列に含まれていないファイルを削除する()
{
// MockFilesystemをFilesRemoverに渡す
$filesystem = new MockFilesystem();
$remover = new App\FilesRemover($filesystem, __DIR__.'/mock-img');
$remover->remove();
// MockFilesystem::remove()で保存したファイル名を確認する
$this->assertSame(
[
__DIR__.'/mock-img/mock-file-del-a.txt',
__DIR__.'/mock-img/mock-file-del-b.txt'
],
$filesystem->getRemoveFiles()
);
}
}
テストがかけた。