如何为Laravel 10中FTP文件下载方法编写单元测试
用PEST PHP为FTP下载类编写单元测试(避免真实数据下载)
我想用PEST PHP编写单元测试,验证以下逻辑:从数据库的producers表获取数据生成FTP配置,随后下载文件并保存到本地。理想做法是创建producer工厂并检查文件是否已下载,但希望完全避免下载真实数据。以下是我的简化类:
class FtpConnection extends Connection { /** * @throws Exception */ public function download() { // get the content and make sure it's utf-8 encoded $file = $this->encode( $this->getFile() ); // save content localy return Storage::disk($this->disk) ->put($this->fileName(), $file); } /** * @throws Exception */ protected function getFile() { $config = $this->config(); $storage = Storage::createFtpDriver($config); throw_if(!$storage->exists($this->producer->path), new Exception('File not found at path: ' . $this->producer->path)); return $storage->get($this->producer->path); } protected function encode($file) { $currentEncoding = mb_detect_encoding($file, 'UTF-8, ISO-8859-1, GBK'); if($currentEncoding != 'UTF-8') { return mb_convert_encoding($file, 'UTF-8', $currentEncoding); } else { return $file; } } protected function config(): array { return [ 'driver' => $this->producer->type, 'host' => $this->producer->host, 'port' => $this->producer->port, 'username' => $this->producer->username, 'password' => Crypt::decryptString($this->producer->password), ]; } }
调用代码:
$ftpConnection = new FtpConnection(Producer::find(1)) $file = $ftpConnection->download();
解决方案
核心思路
单元测试的关键是隔离外部依赖:不需要连接真实FTP服务器、不需要操作真实数据库(可用内存数据库或模型工厂生成测试数据),通过模拟Laravel的Storage和Crypt服务来验证业务逻辑。
1. 准备Producer模型工厂
先创建Producer模型工厂,快速生成测试用数据:
// database/factories/ProducerFactory.php use App\Models\Producer; use Illuminate\Support\Facades\Crypt; return [ 'definition' => [ 'type' => 'ftp', 'host' => 'test-ftp-host', 'port' => 21, 'username' => 'test-user', 'password' => Crypt::encryptString('test-pass'), 'path' => 'test-file.txt', ], ];
2. 编写PEST测试用例
以下是覆盖核心场景的测试代码:
<?php use App\Models\Producer; use App\Services\FtpConnection; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Storage; use Illuminate\Contracts\Filesystem\Filesystem; use ReflectionMethod; test('正确生成FTP配置数组', function () { // 用工厂创建测试用Producer $producer = Producer::factory()->create(); // 反射调用protected的config方法 $ftpConn = new FtpConnection($producer); $configMethod = new ReflectionMethod(FtpConnection::class, 'config'); $configMethod->setAccessible(true); $config = $configMethod->invoke($ftpConn); // 验证配置项是否匹配 expect($config)->toEqual([ 'driver' => $producer->type, 'host' => $producer->host, 'port' => $producer->port, 'username' => $producer->username, 'password' => 'test-pass', // 验证密码解密正确 ]); }); test('下载方法正确处理编码转换并保存文件', function () { // 模拟FTP存储驱动 $mockFtp = Mockery::mock(Filesystem::class); $mockFtp->shouldReceive('exists')->with('test-file.txt')->andReturnTrue(); // 模拟返回ISO-8859-1编码的内容 $rawContent = mb_convert_encoding('测试文本', 'ISO-8859-1', 'UTF-8'); $mockFtp->shouldReceive('get')->with('test-file.txt')->andReturn($rawContent); // 替换Storage的createFtpDriver方法返回模拟实例 Storage::shouldReceive('createFtpDriver')->andReturn($mockFtp); // 模拟本地存储的保存操作 $mockLocal = Mockery::mock(Filesystem::class); $mockLocal->shouldReceive('put') ->with('expected-filename.txt', '测试文本') // 验证编码转换后的内容 ->andReturnTrue(); Storage::shouldReceive('disk')->with('local')->andReturn($mockLocal); // 执行测试 $producer = Producer::factory()->create(); $ftpConn = new FtpConnection($producer); // 假设fileName()方法返回'expected-filename.txt',需根据实际逻辑调整 $result = $ftpConn->download(); expect($result)->toBeTrue(); }); test('FTP文件不存在时抛出异常', function () { // 模拟FTP存储返回文件不存在 $mockFtp = Mockery::mock(Filesystem::class); $mockFtp->shouldReceive('exists')->with('test-file.txt')->andReturnFalse(); Storage::shouldReceive('createFtpDriver')->andReturn($mockFtp); $producer = Producer::factory()->create(); $ftpConn = new FtpConnection($producer); // 验证异常抛出 $this->expectException(Exception::class); $this->expectExceptionMessage('File not found at path: test-file.txt'); $ftpConn->download(); });
关键说明
- 模拟外部服务:用Mockery模拟
Storage的FTP驱动和本地存储,彻底避免真实网络请求 - 反射访问protected方法:对于
config()这类无法直接调用的方法,用反射验证配置生成逻辑 - 编码逻辑验证:模拟非UTF-8的原始内容,验证
encode()方法是否正确转换编码 - 异常场景覆盖:测试文件不存在时的异常抛出逻辑,确保边界情况被覆盖
- 模型工厂:快速生成测试数据,无需手动构造复杂的Producer实例
内容的提问来源于stack exchange,提问作者Greg Ostry
相关产品推荐
相关产品推荐

