【技术博客】Laravel5.1文件上传单元测试

时间:2022-10-19 20:46:59

Laravel5.1文件上传单元测试

作者:ZGJ


在软工第三阶段中,我彻底解决了上一阶段一直困扰我的文件上传单元测试问题,在这里做一个总结。

注:下文介绍中,方法一方法二实现简单但有一定的限制条件(也正因为如此我上一阶段中一直未能实现文件上传的单元测试),方法三即这一阶段摸索出来的方法,普遍性更高。

以下为正文:

方法一:伪造存储

具体见这篇博客

限制条件为laravel版本5.4以上;

方法二:表单交互

具体见这篇博客

限制条件为文件输入的前端元素必须包含于form表单中,否则测试报错;

下面详细介绍方法三:利用call函数

​ 之前的博客中我也提到,由于伪造存储和表单模拟输入的方式均失败,我试图阅读laravel底层代码,通过构造一些信息作为提交post请求的数据进行测试,但是各种方法均未能获得成功,

​ 总结一下失败的根本原因在于:在laravel5.1中,post函数提交的数据仅相当于$_POST全局变量对Request进行实例化,然而在laravel中,hasFile函数的判定使用的是Request实例中的files变量,该变量是利用$_FILE全局变量进行实例化的
​ 但是,laravel中对HTTP请求进行单元测试的原理在于模拟一次请求执行的过程,那么就必定涉及到请求的实例化,因此也一定有files(类似于$_FILE)变量的相关初始化和使用步骤,所以这一阶段中,通过对post,call等方法的源码阅读,我找到了解决该问题的突破口。

首先找到post函数,核心部分为这一句:

$this->call('POST', $uri, $data, [], [], $server);

实则还是调用call函数,因此看到call函数:

public function call($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null)
{
 $kernel = $this->app->make('Illuminate\Contracts\Http\Kernel');

 $this->currentUri = $this->prepareUrlForRequest($uri);

 $request = Request::create(
     $this->currentUri, $method, $parameters,
     $cookies, $files, array_replace($this->serverVariables, $server), $content
 );

 $response = $kernel->handle($request);

 $kernel->terminate($request, $response);

 return $this->response = $response;
}

看到call函数,令我马上眼前一亮的是其中一个参数:$files

再往下看其执行过程,这几乎就是laravel中index.php中的内容,再看其中请求的实例化:

$request = Request::create(
$this->currentUri, $method, $parameters,
$cookies, $files, array_replace($this->serverVariables, $server), $content
);

可以看到在call函数中是可以通过传入$files参数模拟文件上传的(当然还包括其他请求实例化所用的全局变量),如此强大的一个测试函数laravel官方文档中竟然只提到了parameters参数的传入而雪藏了其他的可用参数……

经过进一步的尝试,我彻底的解决了该问题,先付上该部分代码:

$pdf_info = [
 'name' => public_path().'/prepare_pdf/phylab_test.pdf' ,
 'error' => 0 ,
 'type' => 'pdf' ,
 'size' => 100000 ,
 'tmp_name' => public_path().'/prepare_pdf/phylab_test.pdf' ,
] ;
$pdf = new UploadedFile($pdf_info['tmp_name'], $pdf_info['name'], $pdf_info['type'], $pdf_info['size'], $pdf_info['error'] , true);
self::assertTrue($pdf instanceof UploadedFile) ;
$file_arr = [
 'prepare-pdf' => $pdf ,
] ;
$response = $this->call('POST' , '/console/uploadPre' , ['labID'=>'2134'] , [] , $file_arr);
$data = $response->getData() ;
self::assertEquals('上传成功' , $data->message) ;

接下来强调一下务必注意的三个坑点:

坑点1:UploadedFile的实例化必须带有test=true参数

注意这句代码:

$pdf = new UploadedFile($pdf_info['tmp_name'], $pdf_info['name'], $pdf_info['type'], $pdf_info['size'], $pdf_info['error'] , true);

对于UploadedFile的实例化包含6各参数,前五个很容易理解,也就是文件的五项信息,于$_FILE全局变量的结构相对应。最后一项参数不能省略,而且必须传入true,非常重要!

解释一下原因:

文件上传自然涉及到上传后文件的存储,而文件上传的存储涉及到move方法(具体控制器代码可见这篇博客

move方法中首先会利用isValid函数进行判断,如果判断失败,则会直接抛出异常。

再看isValid函数代码:

public function isValid()
{
 $isOk = UPLOAD_ERR_OK === $this->error;

 return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname());
}

注意返回时用到了is_upload_file函数,PHP文档中可以看到如下解释说明:

is_uploaded_file — 判断文件是否是通过 HTTP POST 上传的

如果 filename 所给出的文件是通过 HTTP POST 上传的则返回 TRUE。这可以用来确保恶意的用户无法欺骗脚本去访问本不能访问的文件,例如 /etc/passwd。

​ 这种检查显得格外重要,如果上传的文件有可能会造成对用户或本系统的其他用户显示其内容的话。

​ 为了能使 is_uploaded_file() 函数正常工作,必须指定类似于 [$_FILES'userfile']['tmp_name'] 的变量,而在从客户端上传的文件名 [$_FILES'userfile']['name'] 不能正常运作。

​ 在PHP中,文件上传后有一个暂时存储的路径,相关设置可以在php.ini文件中找到,当然我不会贸然对其进行改动。由于测试中传入的文件名均直接采用服务器上的一个测试文件,所以这个函数的判定将始终返回false。

​ 但是我们注意到,在isValid函数返回的最后,用到了其test参数,如果test参数为true,则会跳过is_upload_file函数的判定,需要注意的是,test参数默认为false,这也就是为什么对UploadedFil初始化时必须带有test=true参数。

坑点2:$files参数的结构

可见代码中作为$files的参数为:

$file_arr = [
'prepare-pdf' => $pdf ,] ;

必须为一个键对应着一个文件实例(文件实例属于类UploadedFile),不可直接将一个文件实例作为$files参数传入

坑点3:对实体文件进行操作需谨慎

​ 该测试中,我直接使用了服务器上的一个pdf测试文件名作为文件实例的文件名,所以相关测试操作也是直接对该实体文件进行操作,测试结束后将该文件还原,以确保不对网站其他功能造成可能的破坏以及下次测试的有效性

​ 以上即是laravel5.1文件上传测试的方法和需要注意的坑点,希望能帮助和我之前一样遇到问题的程序猿朋友,当然,这篇博客的相关内容不能确保完全正确,毕竟这些大部分是自己阅读源码不断尝试探索出的方法,如有错误望指正。