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 協議》,轉載必須註明作者和本文連結