Laravel 中使用集成测试时的数据库设置方法

Laravel框架
384
0
0
2022-11-15

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