關於 phpunit 我會寫一個系列,把我們專案中使用 phpunit 遇到的每一個問題分享給大家。
專案背景:
我們的微服務使用 lumen 搭建,所以這裡的測試都是指的是 api 的測試,而且我們沒有寫任何的單元測試,直接寫的是系統測試,我舉一個例子你就明白了。
/**
* @test
*/
public function itShouldReturnXXXList()
{
factory(XXX::class, 3)->create();
$this->be(new User());
$headers = [
//headers
];
$this->get('/api/xxxs', $headers);
$this->assertResponseOk();
$this->seeJsonStructure([self::XXX_STRUCTURE]);
}
以上的例子是測試在登陸的情況下,獲取 xxx 的列表的介面的返回資料情況。
測試背景
一般在測試的時候,因為每個測試資料庫都要隔離,一般的解決方案有兩種:
-
第一種是使用
transaction
,每次在一個測試開始的時候transaction begin
,在斷言結束後transaction rollback
,這樣一個資料庫中實際沒有寫入任何資料,所以每次測試互不影響,trait
如下:trait DatabaseTransactions { /** * Begin a database transaction. * * @return void */ public function beginDatabaseTransaction() { $this->app->make('db')->beginTransaction(); $this->beforeApplicationDestroyed(function () { $this->app->make('db')->rollBack(); }); } }
-
第二種是使用
migrate
,每次在一個測試開始的時候migrate
,在斷言結束後migrate:rollback
,這樣資料庫每次測試都建表,寫資料,清空資料和表格,所以每次測試也互不影響。trait DatabaseMigrations { /** * Run the database migrations for the application. * * @return void */ public function runDatabaseMigrations() { $this->artisan('migrate'); $this->beforeApplicationDestroyed(function () { $this->artisan('migrate:rollback'); }); } }
我們的測試使用的是
sqlite :memory:
,配置如下:'testing' => [ 'driver' => 'sqlite', 'database' => ':memory:', ],
很遺憾,
sqlite :memory:
不支援transaction
,只能使用migrate
的方式。
問題:
隨著測試越來越多,問題來了,我們一個有一個核心專案的系統測試已經有 97 個了,導致的問題是每次本地測試全部跑一次需要 8 分鐘左右(每個人電腦不同,有些許差別),GitLab 上 ci 需要跑兩次(一次自定義分支提交會跑,一次合併到 master 後會跑),結果就是一個 commit 從提交到稽核程式碼到最終合併上線,需要十幾分鍾,真是的是心好痛。
最大的問題是,很多人一起開發,這個時間是會浪費在每一個人的頭上,所以我們一直想辦法嘗試解決,但是並沒有很好的解決方案。
嘗試解決方案:
嘗試解決方案1(失敗)
因為我們使用的是 migrate
的方式,我們猜想可能使用 transaction
的方式可能會更快一點,而且也確實看到別人使用 mysql transaction
的方式測試速度加快了很多,就試用了一下,但是結果並不理想,甚至更慢。
放棄。
嘗試解決方案2(失敗)
某天我看到 ruby
裡面有比較成熟的並行測試方案,比如 parallel_tests ,覺得這是一個很好的思路,在測試使用多程式的方式來跑,就可以節省大量的時間,想想 php 不可能沒有類似的工具 :),於是我找到了一個 Paratest,你可以透過 https://code.tutsplus.com/tutorials/parall... 對 Paratest
有一定了解。
但是經過試用後,發現出現在不同 php 版本下,Paratest
很不穩定的,偶爾還抽風,決定放棄試用。
期間還遇到了別的類似的工具,但在相容性上都做的很差,略失望。
偶然發現:
偶然我觀察到,在使用 phpunit
來跑全部測試的時候,比較慢的測試都在後面,這很奇怪,如果我使用 phpunit --filter XxxTest
的時候,事實上並沒有那麼慢,這是為什麼?
既然單個檔案跑的時候很快,那我試試一個檔案一個檔案來跑。如下:
for i in $(ls -R ./tests |grep php); do ./vendor/bin/phpunit --configuration phpunit.xml --filter $(echo $i|sed -e "s/.php//g"); done
以上程式碼的意思是從 tests 資料夾中找出所有 php 檔案,最後單個使用 filter
的方式來跑測試,比如有一個檔案是 ExampleTest.php
,最終執行的是 ./vendor/bin/phpunit --configuration phpunit.xml --filter ExampleTest
結果跑下來太震驚了,時間從 8 分 08 秒減到了 47 秒(在我機器),節省超過了 90% 的時間,一個字:”嚇死人“。
嘗試解釋原理:
難道是記憶體的原因嗎?如果是記憶體的原因我在跑 phpunit 的時候,把 memory 調整到 limited,看看效果,
php -d memory_limit=-1 vendor/bin/phpunit --configuration phpunit.xml
事實上,記憶體確實調整到了無限,但是仍然沒有解決問題。
那到底是為什麼呢?為什麼把測試拆到多個檔案,一個一個檔案來跑比全部一起跑會快這麼多?
而且,每個檔案保持測試在 10 個以下,效果更佳。
直到現在,我還不可以更好的解釋原因,如果你有線索,我們可以聊聊。
最終解決方案:
因為我們的 GitLab ci
裡面會跑 phpunit,下面我分享下 .gitlab-ci.yml
配置
script:
- composer install --quiet
- ./phpunit.sh
phpunit.sh,此指令碼是我們架構師 @Sin30 寫的
#!/bin/sh
set -eo pipefail
for i in $(find tests -type f -name "*Test.php" | xargs -I {} basename {} .php)
do
vendor/bin/phpunit --configuration phpunit.xml --filter $i
done
裡面的 set -eo pipfail
解釋以下:
set -e
表示一旦指令碼中命令返回值不是 0,指令碼立即退出;
set -o pipefail
表示在pipe |
中,只要任何一個命令返回值不是 0(假設是 -1),整個 pipe 返回 -1,即使最後一個命令返回 0。
這樣可以保證只要有一個 Test 出錯,後面的就不用再跑了,節省時間。
總結
能節省程式設計師時間的事情是最重要的事情,怎麼強調都不過分。
原文連結:https://www.lijinma.com/blog/2017/01/29/ph...
本作品採用《CC 協議》,轉載必須註明作者和本文連結