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 类定义好入参之类,这样可以做单元测试。要把大方法拆成多个小方法,并利用各种设计模式来实现。