1.测试 csv 文件上传
本系列文章为
laracasts.com
的系列视频教程——Testing Laravel 的学习笔记。若喜欢该系列视频,可去该网站订阅后下载该系列视频,支持正版。
本节说明
- 附录内容为视频课程之外的测试
- 搬运自:Using vfsStream to Test File Uploads with...
本节内容
本节我们来学习测试 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);
}
}
运行测试:
毫无疑问会失败,因为我们还没有添加路由与控制器:
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)
{
}
}
再次运行测试:
现在我们可以来进行读取文件的测试了,首先我们修改控制器:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UploadCsvController extends Controller
{
public function upload(Request $request)
{
$request->file('usersCsvFile')->openFile();
}
}
再次测试:
测试失败了,我们来弄清发生了什么:
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 之前执行,否则的话,在我们得到输出之前,测试就已经失败了。我们再次运行测试:
因为我们并没有上传文件,所以获取的usersCsvFile
为 null
。接下来就该 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
,它是对Symfony
的UploadedFile
的扩展,我们按顺序快速看一下它的参数:
- 第一个参数:文件的完整临时目录;
- 第二个参数:原始的文件名;
- 第三个参数:PHP 所提供的文件类型,如果是 null 的话,默认为 application/octet-stream;
- 第四个参数:文件大小;
- 第五个参数:上传出现错误的常量(PHP 的 UPLOAD_ERR_XXX 常量之一),如果是 null 的话,默认为 UPLOAD_ERR_OK;
- 第六个参数:是否启用测试模式;
现在再次运行测试:
现在,我们让文件更真实一些:
.
.
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"
]);
}
.
.
然后我们来运行测试:
我们来让测试通过:
<?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;
}
}
运行测试:
我们的代码很粗糙,仅仅是为了更快地让测试通过,如果你想做点重构,现在正是时候,测试为你提供了充足的信心。首先,我们将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);
}
}
运行测试:
接下来我们新增一个测试,我们只接受上传 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)。运行测试:
我们将创建一个 表单请求 来处理我们的验证规则。表单请求是包含验证逻辑的自定义请求类,我们运行以下命令来创建表单请求类:
$ 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;
}
}
现在我们来运行测试:
注:刚好看到这篇文章的翻译版(组合使用 Laravel 和 vfsStream 测试文件上传),于是就添加到这个系列当中。