Laravel 中使用整合測試時的資料庫設定方法

yyy123456發表於2022-09-20

Laravel 中使用整合測試時的資料庫設定方法

需求

其實官方文件有說的很仔細,但是我不太想用文件的方式,原因是比較慢,比較重,沒有必要,希望能簡單一些使用。
官方的方式是 use RefreshDatabase; 這其實是利用資料遷移的語句修改資料庫,我不希望如此,我希望遷移僅僅是遷移,不用於其他用途。

為了寫文件,得構建一個例子,比如有一張活動表,希望查出有效的活動有多少個,然後整合測試。
版本上,我自己版本較低是5.8,新一些可以用8.5,或更高,程式碼基本不變。

檔案數量

測試類的抽象父類 ,是本文的主要內容,Tests\DatabaseTestCase.php
測試類,主要內容,Tests\Feature\ActControllerTest.php
路由檔案,api.php
資料遷移檔案,2022_09_20_152556_create_wws_activity_table.php
控制器程式,App\Http\Controllers\Api\ActController.php
模型類,必須得有,忽略,Activity.php
模型工廠類,必須得有,忽略。ActivityFactory.php

實現方式

自己編寫一個父類,有資料庫測試需求,就繼承這個父類。
但如果自己寫的單元測試中不涉及資料庫,就直接繼承 TestCase 類。
整個過程是自己先清表,再插入假資料(就是 phpunit 官方文件中說的“建立基境”),再發起 http 請求,再斷言驗證。

// 這是測試類的父類 Tests\DatabaseTestCase.php
<?php

namespace Tests;

use DB;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Exception;

abstract class DatabaseTestCase extends TestCase
{


    //這個是自己資料庫的表字首,沒有就空字串。
    const TABLE_PREFIX = 'wws_';


    // 強制子類實現這個方法。
    abstract public function get_table_datas();


    public $all_model;


    /**
     *
     * 查詢專案中的所有模型類。
     * 根據這個類是否是 Model類的子類,以及不是抽象類,來判斷,自己也可以隨意補充。
     *
     * @return Collection
     */
    public function getModels(): Collection
    {
        $models = collect(File::allFiles(app_path()))
            ->map(function ($item) {
                $path = $item->getRelativePathName();
                $class = sprintf('\%s%s',
                    Container::getInstance()->getNamespace(),
                    strtr(substr($path, 0, strrpos($path, '.')), '/', '\\'));

                return $class;
            })
            ->filter(function ($class) {
                $valid = false;

                if (class_exists($class)) {
                    $reflection = new \ReflectionClass($class);
                    $valid = $reflection->isSubclassOf(Model::class) &&
                        !$reflection->isAbstract();
                }

                return $valid;
            });

        return $models->values();
    }

    /**
     * 根據某個表名,得到一個模型類
     *
     * @param $table_name
     * @return mixed
     * @throws Exception
     */
    public function get_class_by_class_name($table_name)
    {
        foreach ($this->all_model as $model_name) {
            $model = new $model_name();
            if ((self::TABLE_PREFIX . $model->getTable()) == $table_name) {
                // 這裡,只要找到一個類,就立刻返回,有時我們會給一張表兩個模型類,
                //可以透過在模型類中加入特定的欄位來識別,
                return $model;
            }

        }
        throw new Exception('該表名對應的模型類沒有找到 : ' . $table_name);
    }


    /**
     *
     *
     * @throws Exception
     */
    public function setUp(): void
    {
        parent::setUp(); // 必須繼承父類

        // 讀取並解析。
        $str = $this->get_table_datas();
        $arr = yaml_parse($str); //這是 php 自帶函式

        $this->all_model = $this->getModels();
        foreach ($arr as $table_name => $rows) {

            $sql = "truncate table {$table_name}";
            DB::statement($sql);

            $model = $this->get_class_by_class_name($table_name);
            // 利用工廠類插入資料,工廠類必須有。而且工廠類不能有after之類的操作。就是最簡單的工廠類。
            if ($rows) {
                foreach ($rows as $row) {
                    factory(get_class($model))->create($row);
                }
            }
        }

    }

}
// 測試類 ActControllerTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\DatabaseTestCase;
use Tests\TestCase;

class ActControllerTest extends DatabaseTestCase
{

    // use RefreshDatabase;

    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_get_valid_act()
    {

        // 這是斷言介面的返回。
        $response = $this->get('/api/activity/get_valid_act');

        $response->assertStatus(200)
            ->assertJson([
                'valid_act_count' => 2, //下面的插入資料,活動有兩個是有效的。
            ]);

        // 順便也可以直接斷言資料庫
        $this->assertDatabaseHas('activity', ['activity_name' => '分類1']);
        $this->assertDatabaseMissing('activity', ['activity_name' => '分類100']);
    }

    // 注意這裡,雖然下面只是一張表,實際可以多張表的資料一起插入
    //由父類程式碼可知,先插入資料庫,後執行測試。
    public function get_table_datas()
    {

        return <<<sql
wws_activity:
  -
    id: 1
    activity_name: "分類1"
    activity_description: ""
    image: ""
    start: 1
    end: 2
    type: 1
    status: 0
    limit: 0
  -
    id: 2
    activity_name: "分類2"
    activity_description: ""
    image: ""
    start: 1
    end: 2
    type: 1
    status: 1
    limit: 0
  -
    id: 3
    activity_name: "分類3"
    activity_description: ""
    image: ""
    start: 1
    end: 2
    type: 1
    status: 1
    limit: 0

sql;
    }

}
路由檔案
Route::any('/activity/get_valid_act', 'Api\ActController@get_valid_act');
// 活動表的遷移檔案
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateWwsActivityTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('activity', function (Blueprint $table) {
            $table->increments('id');
            $table->string('activity_name', 100)->comment('活動名稱');
            $table->text('activity_description', 16777215)->comment('活動描述');
            $table->string('image', 500)->comment('活動圖片');
            $table->timestamps();
            $table->integer('start')->unsigned()->comment('活動開始時間或者報名開始時間');
            $table->integer('end')->unsigned()->comment('活動結束時間或者報名結束時間');
            $table->boolean('type')->nullable()->comment('活動型別:');
            $table->boolean('status')->comment('狀態,0為未生效,1生效,2稽核不透過,3下架');
            $table->boolean('limit')->default(0)->comment('限制次數或者人數,0不限制,大於1,表示限制資料');
            $table->string('comment', 200)->default('')->comment('稽核不透過的原因,新增的備註欄位');
            $table->string('mini_programs', 500)->default('')->comment('');
            $table->boolean('show_center')->default(0)->comment('');
            $table->integer('sort')->default(0)->comment('活動的顯示排序,從小到大');
            $table->string('regular_json', 2000)->default('')->comment('活動規則表');
            $table->boolean('display_equipment')->default(1)->comment('');
            $table->bigInteger('merchant_user_id')->unsigned()->default(0)->comment('預設為0,代表後臺新增');
            $table->boolean('iframe_exists')->default(0)->comment('該活動是否需要小程式首頁彈框,0不需要,1需要');
            $table->string('iframe_img')->default('')->comment('該活動的小程式首頁彈框的圖片');
        });
    }


    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('wws_activity');
    }

}
// 這是控制器程式
<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Activity;
use Illuminate\Http\Request;

/**
 * 活動控制器
 */
class ActController extends Controller
{
    public function get_valid_act(Request $request)
    {
        $count = Activity::query()
            ->where('status', 1)
            ->count();

        $json = [
            'code' => 0,
            'valid_act_count' => $count,
        ];

        return response()->json($json);

    }
}

實現效果截圖

命令是 ./vendor/bin/phpunit ./tests/Feature/ActControllerTest.php

總結

1、本文不使用 laravel 官方的資料庫測試語句 ,被我註釋掉,// use RefreshDatabase;
2、網上有資料顯示,sqlite 資料庫更慢,還是 mysql 好使。
3、本文使用 yaml 檔案編排假資料。
4、本文利用反射,查詢模型類,並插入假資料。
5、建議編寫.env.tesing 或直接給 phpunix.xml 新增 <server name=”DB_DATABASE” value=”專用於單元測試的庫名”/> ,這樣的好處是本機有兩個資料庫,一個正常用,一個專用於單元測試和整合測試,後者每次測試前都會自動清表。當然,兩個庫的表結構得一致。
6、測試程式碼和 phpunix.xml 檔案得和正式程式放一起,因為這是一個整體,都放同一個 git 裡。
7、複雜的邏輯應該寫 Service 類,然後在控制器中呼叫 Service 類實現功能,同時給 Service 類定義好入參之類,這樣可以做單元測試。要把大方法拆成多個小方法,並利用各種設計模式來實現。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章