做者:ZGJphp
版本:v1.0
前端
PM注:本人這兩天也正在嘗試解決這一問題,若有進展將及時更新這一博客laravel
在咱們的軟工第二階段中,我開始着手進行後端控制器的單元測試,然而一個階段下來,其中的一個涉及文件上傳的控制器方法始終沒能覆蓋徹底,這裏總結我在此測試上進行的若干嘗試(均未得到成功),先附上被測試控制器方法代碼:json
public function uploadPreparePdf() { $exists = Auth::check() && ((Console::where('email', '=', Auth::user()->email)->get()->count()) > 0); $isAdmin = $exists; if (!$isAdmin) { return redirect('/index'); } $data = ["status"=>"","message"=>""]; if (Request::hasFile('prepare-pdf')) { $pdfFile = Request::file('prepare-pdf'); $labID = $_POST['labID']; if (preg_match('/^pdf$/', $pdfFile->getClientOriginalExtension()) && $pdfFile->getSize() < Config::get('phylab.maxUploadSize')) { $fname = $labID . '.pdf'; //TODO use lab id instead $pdfFile->move(Config::get('phylab.preparePath'), $fname); $data['status']=SUCCESS_MESSAGE; $data['message']="上傳成功"; } else { $data["status"]=FAIL_MESSAGE; $data["message"] = "上傳失敗,文件格式或大小不符合要求!"; } } else { $data["status"]=FAIL_MESSAGE; $data["message"] = "上傳失敗,沒有找到文件!"; } return response()->json($data); }
測試失敗的緣由是第二個if語句hasFile()的斷定始終爲false……也就是這一句:後端
if (Request::hasFile('prepare-pdf'))數組
另外附上前端代碼,這裏有一點須要注意的是,前端文件輸入的元素並未包含在form表單中,這也致使了其中一種測試方法的失敗,後面在嘗試2中會說起。框架
<div class="form-group"> <input type="file" id="input-prepare-pdf" name="prepare-pdf"> <p class="help-block">請務必先選擇一個實驗,文件類型限制爲PDF,大小不能超過5M</p> </div> <div class="modal-footer" style="margin-top: 0;padding-bottom: 0"> <div style="float:right"> <button type="submit" id="btn-upload-preview" class="btn btn-large btn-danger">上傳</button> </div> </div>
如下開始詳細闡述我爲了解決該問題進行的若干思路和嘗試:ide
經過查閱相關資料,我瞭解到laravel對於文件上傳測試有較爲便利的原生支持,可經過「僞造」磁盤和文件的方式進行文件上傳的測試,像這樣:函數
Storage::fake('avatars');
$response = $this->json('POST', '/avatar', [
'avatar' => UploadedFile::fake()->image('avatar.jpg')]);post
然而尷尬的是,這個原生支持僅針對laravel5.4以上的版本,而咱們的……則是上古版本5.1……
(要不是時間關係,我真的好想進行一次升級並重構,這5.1的版本實在是太老,無奈……)
這種最爲實用並且簡單的測試方法宣告失敗。
經過查看laravel5.1的文檔,能夠發現文件上傳測試還能夠經過表單交互的方式進行,如這樣:
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->name('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
文檔中對於attach和press函數的介紹是這樣的:
Method | Description |
---|---|
$this->attach($pathToFile, $elementName) | "Attach" a file to the form. |
$this->press($buttonTextOrElementName) | "Press" a button with the given text or name. |
很淺顯,很少作解釋,這兩個函數在其餘測試中我亦使用過,然而在這裏進行測試時,會產生以下報錯:
報錯行顯示在press函數,緣由是前端文件輸入並未包含在form表單中(見上文中的前端代碼),可是因爲對前端修改還須要牽扯到其餘的一些部分,況且我認爲爲了方便測試而修改代碼並非一個好習慣(我的理解)。
因此這一種嘗試也宣告失敗……
爲何進行這一項嘗試,這就涉及到laravel請求生命週期的相關知識。
這裏僅闡述請求實例化的部分,laravel中請求實例化位於文件Illuminate\Http\Request.php中
/** * Create a new Illuminate HTTP request from server variables. * * @return static */ public static function capture() { static::enableHttpMethodParameterOverride(); return static::createFromBase(SymfonyRequest::createFromGlobals()); }
而經過createFromBase方法的代碼(這裏就不貼了)又能發現laravel框架的請求實例是在Symfony請求實例的基礎上建立的。
對此,我又找到vendor\symfony\http-foundation\Request.php文件
其中關於請求的實例化有一句關鍵代碼:
$request = self::createRequestFromFactory($_GET, $_POST, array(), $_COOKIE, $_FILES, $server);
也就是說,這是經過PHP的全局變量建立一個新的請求實例,若是沒有laravel框架,PHP對於文件上傳以及測試能夠直接操做$_FILE全局變量是確定沒有問題的,laravel對其封裝在Request實例中,故我想到能否在發送post請求以前修改$_FILE,即這樣:
(多說起一點,$_FILE全局變量的結構是一個二維數組,每個元素有五個字段,具體可查閱相關PHP文檔)
$pdf_info = [ 'name' => public_path().'/prepare_pdf/phylab_test.pdf' , 'error' => 0 , 'type' => 'pdf' , 'size' => 100000000 , 'tmp_name' => public_path().'/prepare_pdf/phylab_test.pdf' , ] ; $_FILES["prepare-pdf"]["name"] = $pdf_info['name'] ; $_FILES["prepare-pdf"]['error'] = $pdf_info['error'] ; $_FILES["prepare-pdf"]['type'] = $pdf_info['type'] ; $_FILES["prepare-pdf"]['tmp_name'] = $pdf_info['tmp_name'] ; $_FILES["prepare-pdf"]['size'] = $pdf_info['size'] ; //Request::capture() ; //self::assertTrue(Request::hasFile('prepare-pdf')) ; $this->post('/console/uploadPre') ->seeJson([
爲了驗證以前對於請求實例化代碼的理解,可見當中有兩句註釋代碼:
//Request::capture() ; //self::assertTrue(Request::hasFile('prepare-pdf')) ;
這兩句代碼(取消註釋後)是能夠正常經過測試的。
遺憾的是,測試中提交post請求後,後端控制器方法中fasFile()的斷定依舊爲false。
這一種嘗試,失敗……
既然測試始終沒法覆蓋徹底的緣由是hasFile方法判斷爲false,那我就看看這一個函數是如何進行的,故我找到Request.php中hasFile方法的源碼:
public function hasFile($key) { if (! is_array($files = $this->file($key))) { $files = [$files]; } foreach ($files as $file) { if ($this->isValidFile($file)) { return true; } } return false; }
首先我看到的是返回條件即$this->isValidFile($file)這一句:
在此找到isValidFile()這個方法:
protected function isValidFile($file) { return $file instanceof SplFileInfo && $file->getPath() != ''; }
因此我想到了構造SplFileInfo類的實例並做爲post請求的數據發送一個post請求:
$pdf = new SplFileInfo('prepare_pdf/phylab_test.pdf') ; $this->post('/console/uploadPre' , [ 'prepare-pdf' => $pdf , ])->seeJson([
很遺憾,這一種嘗試依舊失敗……
先說起一句,Request::file 方法返回來的對象是 Symfony\Component\HttpFoundation\File\UploadedFile 類的一個實例,這個類繼承了 PHP 的 SplFileInfo 類,固然進行這個嘗試更重要的緣由在後面:
在上一項嘗試中,我發現hasFile()方法在進行返回判斷以前,有這樣一句代碼:
$files = $this->file($key)
找到file()方法:
public function file($key = null, $default = null) { return Arr::get($this->files->all(), $key, $default); }
在返回調用Arr::get方法時第一個參數比較重要,即$this->files->all() ,
這裏又須要瞭解Request實例中的$files屬性,找到其源頭,位於文件vendor\symfony\http-foundation\Request.php中,而$file的初始化是這樣的:
$this->files = new FileBag($files);
傳入的參數即爲PHP中的$_FILE全局變量,這也就和我前面所說的請求實例化聯繫到了一塊兒,對此,我再次找到FileBag類的源碼,其中對於FileBag類的構造進行了多層調用,一層層找到其根源,有這樣一句:
parent::set($key, $this->convertFileInformation($value));
而經過對convertFileInformation()函數的解讀:
protected function convertFileInformation($file) { if ($file instanceof UploadedFile) { return $file; } $file = $this->fixPhpFilesArray($file); if (is_array($file)) {
能夠發現,這個函數接收的參數有兩種,一種是UploadedFile實例,另外一種是一個文件信息的數組,並經過這個數組建立一個UploadedFile實例:
$file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['size'], $file['error']);
**這也就和$_FILE全局變量的結構聯繫到了一塊兒。**
經過改變$_FILE的方式前面已經試過,失敗,那麼我能不能經過傳入UploadedFile實例的方式提交post請求,即這樣:
$pdf = new UploadedFile($pdf_info['tmp_name'], $pdf_info['name'], $pdf_info['type'], $pdf_info['size'], $pdf_info['error']); $this->post('/console/uploadPre' , [ 'prepare-pdf' => $pdf , ])->seeJson([
失望的是,這種嘗試,仍然失敗……
以上即是我針對laravel5.1文件上傳進行的幾項主要嘗試的過程,固然實際上還有一些看似就不靠譜的小嚐試(抱着幻想仍是要試一下),那些就不拿出來獻醜了哈哈…
雖然有一些綜合的緣由,可是終究仍是本身學術不精。若是有大佬知道解決的方案,或者是我以上部分的理解存在一些問題,歡迎指正,感激涕零!