[phpunit] 這樣跑測試,竟然節省了我們 90% 的時間

lijinma發表於2017-01-29

關於 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 的列表的介面的返回資料情況。

測試背景

一般在測試的時候,因為每個測試資料庫都要隔離,一般的解決方案有兩種:

  1. 第一種是使用 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();
        });
    }
    }
  2. 第二種是使用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 協議》,轉載必須註明作者和本文連結
寫文字大部分時候是因為我希望能幫助到你,小部分時候是想做總結或做記錄。我的微信是 lijinma,希望和你交朋友。 以下是我的公眾賬號,會分享我的學習和成長。

相關文章