1.测试 csv 文件上传

未匹配的标注

本系列文章为laracasts.com 的系列视频教程——Testing Laravel 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频,支持正版

本节说明

本节内容

本节我们来学习测试 csv 文件上传并读取内容。我们将使用 vfsStream 来帮助我们进行测试:

vfsStream 允许程序与保存在内存中的文件进行交互,而不是直接与磁盘中实际存在文件进行交互。因为它处理的是内存而不是物理的硬件驱动器,所以它会更快。

首先我们进行安装:

$ composer require mikey179/vfsStream --dev

接下来我们新建测试:

$ php artisan make:test UploadCsvTest

添加我们的第一个测试:

tests\Feature\UploadCsvTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class UploadCsvTest extends TestCase
{
    use WithoutMiddleware;

    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', ['usersCsvFile' => 'someFile']);

        $response->assertStatus(200);
    }
}

运行测试:
file
毫无疑问会失败,因为我们还没有添加路由与控制器:

routes\web.php

.
.
Route::post('/upload-new-users-csv','UploadCsvController@upload');

app\Http\Controllers\UploadCsvController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UploadCsvController extends Controller
{
    public function upload(Request $request)
    {

    }
}

再次运行测试:
file
现在我们可以来进行读取文件的测试了,首先我们修改控制器:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UploadCsvController extends Controller
{
    public function upload(Request $request)
    {
        $request->file('usersCsvFile')->openFile();
    }
}

再次测试:
file
测试失败了,我们来弄清发生了什么:

tests\Feature\UploadCsvTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;

class UploadCsvTest extends TestCase
{
    use WithoutMiddleware;

    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', ['usersCsvFile' => 'someFile']);

        dd($response->getContent());

        $response->assertStatus(200);
    }
}

需要注意地是,dd() 要在断言响应 OK 之前执行,否则的话,在我们得到输出之前,测试就已经失败了。我们再次运行测试:
file
因为我们并没有上传文件,所以获取的usersCsvFilenull。接下来就该 vfsStream 一展身手了。我们先将创建文件的代码逻辑放到测试文件的一个函数中:

tests\Feature\UploadCsvTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\UploadedFile;
use org\bovigo\vfs\vfsStream;

class UploadCsvTest extends TestCase
{
    use WithoutMiddleware;

    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', ['usersCsvFile' => $this->createUploadFile()]);

        $response->assertStatus(200);
    }

    protected function createUploadFile()
    {
        $vfs = vfsStream::setup(sys_get_temp_dir(), null, ['testFile' => 'someString']);

        return new UploadedFile(
            $vfs->url().'/testFile',
            'test',
            'text/csv',
            null,
            null,
            true
        );
    }

}

首先是vfsStream::setup,它会在内存中创建一个文件。样例中使用了sys_get_temp_dir(),这只是让它看起来更真实一些,这里可以使用任意的值,比如可以将其修改为字符串root,运行效果是完全一样的。这背后的理念是允许你创建任意的目录结构以适应实际的需求。

下一个参数用来设置文件权限,在本例中,我们没有特殊的要求,所以只需将其设置为null,即允许对文件进行任意操作。最后,是目录中的内容,它只是一个数组,由文件名(key)和值所组成。

最后,我们可以看到的是 UploadedFile,它是对SymfonyUploadedFile的扩展,我们按顺序快速看一下它的参数:

  • 第一个参数:文件的完整临时目录;
  • 第二个参数:原始的文件名;
  • 第三个参数:PHP 所提供的文件类型,如果是 null 的话,默认为 application/octet-stream;
  • 第四个参数:文件大小;
  • 第五个参数:上传出现错误的常量(PHP 的 UPLOAD_ERR_XXX 常量之一),如果是 null 的话,默认为 UPLOAD_ERR_OK;
  • 第六个参数:是否启用测试模式;

现在再次运行测试:
file
现在,我们让文件更真实一些:


    .
    .
    protected function createUploadFile()
    {
        $vfs = vfsStream::setup(sys_get_temp_dir(), null, ['testFile.csv' => '']);

        $uploadFile = new UploadedFile(
            $vfs->url().'/testFile.csv',
            'test',
            'text/csv',
            null,
            null,
            true
        );

        collect([
            ['username', 'first name', 'last name'],
            ['jondoe', 'Jon', 'Doe'],
            ['janedoe', 'Jane', 'Doe']
        ])->each(function ($fields) use ($uploadFile) {
            $uploadFile->openFile('a+')->fputcsv($fields);
        });

        return $uploadFile;
    }
}

接下来我们要测试能够读取文件中的内容,并返回 JSON 响应。首先我们来修改测试:

    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', ['usersCsvFile' => $this->createUploadFile()]);

     $response->assertStatus(200);

        $response->assertJson([
            "username,\"first name\",\"last name\"\n","jondoe,Jon,Doe\n","janedoe,Jane,Doe\n"
        ]);
    }
    .
    .

然后我们来运行测试:
file
我们来让测试通过:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class UploadCsvController extends Controller
{
    public function upload(Request $request)
    {
        $uploadedFile = $request->file('usersCsvFile')->openFile();

        $returnArray = collect();
        while (!$uploadedFile->eof()) {
            $returnArray->push($uploadedFile->fgets());
        }

        return $returnArray;
    }
}

运行测试:
file
我们的代码很粗糙,仅仅是为了更快地让测试通过,如果你想做点重构,现在正是时候,测试为你提供了充足的信心。首先,我们将createUploadFile()函数做得更加具有可重用性:

.
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamFile;
.
.
protected function createUploadFile(vfsStreamFile $file,$originalName)
{
    return new UploadedFile(
        $file->url(),
        $originalName,
        mime_content_type($file->url()),
        null,
        null,
        true
    );

}
.
.

这样,我们就能够使用vfsSream来创建文件了,它能够知道文件位于何处(内存中),并且能够确定所创建文件的 MIME 类型。接下来,我们就能创建虚拟文件了:

.
.
protected function createVirtualFile($filename, $extension)
{
    return vfsStream::setup(sys_get_temp_dir(), null, [$filename.'.'.$extension => '']);
}
.
.

createVirtualFile()根据给定的文件名和扩展名创建了一个虚拟文件,方便我们在后续的测试中,能够随意地创建 CSV 以及其他类型的文件。然后,我们还需要一个函数,它使用上面提到的这些函数来创建一个 CSV 文件:

    .
    .
    protected function createCsvUploadFile($fileName = 'testFile')
    {
        $virtualFile = $this->createVirtualFile($fileName, 'csv')->getChild($fileName.'.csv');

        $fileResource = fopen($virtualFile->url(), 'a+');
        collect([
            ['username', 'first name', 'last name'],
            ['jondoe', 'Jon', 'Doe'],
            ['janedoe', 'Jane', 'Doe']
        ])->each(function ($fields) use ($fileResource) {
            fputcsv($fileResource, $fields);
        });
        fclose($fileResource);

        return $this->createUploadFile($virtualFile,$fileName);
    }
    .
    .

在这个函数中,首先,我们根据给定的名字创建了一个 CSV 文件,然后调用了getChild()函数,它的作用是从虚拟系统中得到虚拟文件,这样,我们就能以更加直接的方式来使用文件。在前面的代码中,不需要这样做,因为在那时我们直接将文件路径提供给了UploadFile

现在我们完整的测试文件如下:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Http\UploadedFile;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamFile;

class UploadCsvTest extends TestCase
{
    use WithoutMiddleware;

    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', [
            'usersCsvFile' => $this->createCsvUploadFile()
            ]);

        $response->assertStatus(200);

        $response->assertJson([
            "username,\"first name\",\"last name\"\n","jondoe,Jon,Doe\n","janedoe,Jane,Doe\n"
        ]);
    }

    protected function createUploadFile(vfsStreamFile $file,$originalName)
    {
        return new UploadedFile(
            $file->url(),
            $originalName,
            mime_content_type($file->url()),
            null,
            null,
            true
        );

    }

    protected function createVirtualFile($filename, $extension)
    {
        return vfsStream::setup(sys_get_temp_dir(), null, [$filename.'.'.$extension => '']);
    }

    protected function createCsvUploadFile($fileName = 'testFile')
    {
        $virtualFile = $this->createVirtualFile($fileName, 'csv')->getChild($fileName.'.csv');

        $fileResource = fopen($virtualFile->url(), 'a+');
        collect([
            ['username', 'first name', 'last name'],
            ['jondoe', 'Jon', 'Doe'],
            ['janedoe', 'Jane', 'Doe']
        ])->each(function ($fields) use ($fileResource) {
            fputcsv($fileResource, $fields);
        });
        fclose($fileResource);

        return $this->createUploadFile($virtualFile,$fileName);
    }
}

运行测试:
file

接下来我们新增一个测试,我们只接受上传 csv 格式问题,当上传其他类型的文件,比如 img 时,我们不通过:

    .
    .
    /** @test */
    public function it_can_upload_csv_successfully()
    {
        $response = $this->json('POST', 'upload-new-users-csv', [
            'usersCsvFile' => $this->createCsvUploadFile()
            ]);

        $response->assertStatus(200);

        $response->assertJson([
            "username,\"first name\",\"last name\"\n","jondoe,Jon,Doe\n","janedoe,Jane,Doe\n"
        ]);
    }

    /** @test */
    public function it_can_only_upload_csv_regardless_of_extension()
    {
        $response = $this->json('POST', 'upload-new-users-csv', [
            'usersCsvFile' => $this->createImageUploadFile('testFile', 'csv')
        ]);

        $response->assertStatus(422);
    }
    .
    .

我们试图创建带有 csv 扩展名的图片,并且要确保该图片无法欺骗我们的上传功能。创建函数如下:

    .
    .
    /** @test */
    public function it_can_only_upload_csv_regardless_of_extension()
    {
        $response = $this->json('POST', 'upload-new-users-csv', [
            'usersCsvFile' => $this->createImageUploadFile('testFile', 'csv')
        ]);

        $response->assertStatus(422);
    }

    protected function createImageUploadFile($fileName = 'testFile', $extension = 'jpeg')
    {
        $virtualFile = $this->createVirtualFile($fileName, $extension)
                    ->getChild($fileName.'.'.$extension);

        imagejpeg(imagecreate(500, 90), $virtualFile->url());

        return $this->createUploadFile($virtualFile,$fileName);
    }
    .

我们试图上传带有 csv 后缀的图片文件,并且预期得到 422 错误(Unprocessable Entity)。运行测试:
file

我们将创建一个 表单请求 来处理我们的验证规则。表单请求是包含验证逻辑的自定义请求类,我们运行以下命令来创建表单请求类:

$ php artisan make:request UploadCsvUsers

验证规则是如何运行的呢?你所需要做的就是在控制器方法中注入传入的请求类型。在调用控制器方法之前验证传入的表单请求,这意味着你不需要在控制器中写任何验证逻辑。我们将该文件修改如下:

app\Http\Requests\UploadCsvUsers.php

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class UploadCsvUsers extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'usersCsvFile' => 'required|mimes:csv,txt'
        ];
    }
}

然后在控制器中注入:

app\Http\Controllers\UploadCsvController.php

<?php

namespace App\Http\Controllers;

use App\Http\Requests\UploadCsvUsers;

class UploadCsvController extends Controller
{
    public function upload(UploadCsvUsers $request)
    {
        $uploadedFile = $request->file('usersCsvFile')->openFile();

        $returnArray = collect();
        while (!$uploadedFile->eof()) {
            $returnArray->push($uploadedFile->fgets());
        }

        return $returnArray;
    }
}

现在我们来运行测试:
file

注:刚好看到这篇文章的翻译版(组合使用 Laravel 和 vfsStream 测试文件上传),于是就添加到这个系列当中。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:1
讨论数量: 0
发起讨论 查看所有版本


暂无话题~