You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 05:10:20