Laravel 结合数据库做PHP单元测试,并且 migrate 和 seed 只做一次


作者: 我不是鱼 (2018-11-13 13:55) 分类: PHP   标签: Laravel PHPUnit

做测试的时候难免要结合数据库来测试,为了不影响现有数据及保持测试数据的完整性,一般是每次测试的时候创建一个新的测试数据库,并把写好的测试数据导入。这里就要用到 Laravel 的 Migrations 和 Seeding,请参考:
https://laravel.com/docs/master/migrations
https://laravel.com/docs/master/seeding

Laravel 已经整合 PHPunit,我们要做的就是建好测试数据库及表,导入测试数据,写测试函数。这里为了测试方便,用 sqlite 代替 MySQL 做测试数据库,避免每次都要启动 MySQL 来测试的麻烦。

首先在 database 目录下创建一个 testing.database.sqlite 文件作为 sqlite 数据库文件

touch database/testing.database.sqlite


然后在 config/database.php 的 connections 中加入测试数据库

    'connections' => [

        // For php unix test
        'testing' => [
            'driver' => 'sqlite',
            'database' => database_path('testing.database.sqlite'),
            'prefix' => '',
        ],

        'sqlite' => [

 

注意这里 key 为 testing,这个后面有用到。

数据库好了后然后创建数据表,文件在 database/migrations 下,可以通过如下命令创建,例如要创建一个 blog 表:

php artisan make:migration create_blog_table

会根据时间生成类似这样的一个文件:2018_11_13_055220_create_blog_table.php
在里边加入表的字段,我的例子如下:

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

class CreateBlogTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('blog', function (Blueprint $table) {
            $table->increments('id');
            $table->char('title', 200);
            $table->char('slug', 200)->unique()->nullable()->default(null);
            $table->integer('user_id')->unsigned()->nullable()->default(null);
            $table->integer('posted')->unsigned()->default(0);
            $table->integer('created_at')->unsigned();
            $table->integer('updated_at')->unsigned()->nullable()->default(null);
        });
    }

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

 

表建好后,准备测试数据,文件在 database/seeds 目录下。可以通过如下命令生成,例如

php artisan make:seeder BlogTableSeeder


就是生成一个 BlogTableSeeder.php,在里边插入测试数据,我的例子如下

use App\Models\Blog;
use Illuminate\Database\Seeder;

class BlogTableSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        Blog::create([
            'title' => 'lnmp test blog',
            'slug'  => 'lnmp-test-blog',
            'user_id' => 1,
        ]);
    }
}

 

然后在 DatabaseSeeder.php 的 run 中加入

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(BlogTableSeeder::class);
    }
}


注意:新加的 BlogTableSeeder.php 类要执行 

composer dump-autoload

否则有可能会找不到这个类。https://laravel.com/docs/5.7/seeding#running-seeders


附上 App\Models\Blog

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    protected $table = 'blog';

    protected $guarded = ['id'];

    protected $dateFormat = 'U';

    protected $casts = [
        'created_at' => 'timestamp',
        'updated_at' => 'timestamp',
    ];

    /**
     * Determine a slug exists or not
     *
     * @param string $slug
     *
     * @return bool
     */
    public function hasSlug($slug)
    {
        return $this->newQuery()->where('slug', $slug)->exists();
    }
}

 

表有了,数据也有了,现在写测试函数。测试函数写在 tests/Unit 或者 tests/Feature 下。可以通过如下命令在 Unit 目录生成 ModelTest.php,例如

php artisan make:test ModelTest --unit

 

ModelTest.php 内容如下

namespace Tests\Unit;

use App\Models\Blog;
use Tests\TestCase;

class ModelTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBlog()
    {
        $blogModel = new Blog();
        $blog = $blogModel->newQuery()->create([
            'title' => 'lnmp another test blog',
            'slug'  => 'lnmp-another-test-blog',
            'user_id' => 1,
        ]);

        $this->assertNotEmpty($blog);
        $this->assertTrue($blog->id > 0);
        $this->assertTrue(is_numeric($blog->created_at));

        $this->assertTrue($blogModel->hasSlug('lnmp-another-test-blog'));
        $this->assertFalse($blogModel->hasSlug('lnmp-another-test-blog2'));
    }
}

 

好了,现在测试函数写好了,在测试之前还有两件事要做,一是指定测试数据库是 sqlite,二是测试之前导入数据。

指定测试数据库 sqlite 有两中方法。

一可以新增一个 .env.testing 的文件,里边的 DB_CONNECTION 改成 testing。其中 testing 就是上面提到的加入到 database config 的那个 key。

DB_CONNECTION=testing

另外一种方法是在 phpunit.xml 中添加,类似

<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
    <!-- ... -->

    <php>
        <env name="DB_CONNECTION" value="testing" />
    </php>
</phpunit>

这里我选择用 .env.testing 进行覆盖。


接着导入测试数据,想法是在测试开始前清除数据,然后重新导入数据。

刷新表可以用命令

php migrate:fresh --env=testing --database=testing

Laravel 也提供现成的 trait 去刷新表:

use Illuminate\Foundation\Testing\RefreshDatabase;

只要在 Tests/TestCase.php 加入就会每次 setUp 的时候会自动清理数据

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;
    use RefreshDatabase;
}

但这样有个问题就是清理之后并没有执行 seed 导入数据。还是要在 setUp 方法中再执行 seed 一次。并且是每个测试文件都会重复创建数据库,创建表,导入数据,测试后删除数据。这样如果测试文件比较多,还是相当影响速度的,而且有些文件可能也不需要导入数据测试。所以这里想法是让 PHPUnit 执行所有测试前清理并导入数据,所有测试只做一次 migrate 和 seed。

既然现成的 trait 无法满足同时 migrate 和 seed。这里还是决定用执行命令行命令去做。 migrate 和 seed 一起做的命令是

php artisan migrate:fresh --seed --env=testing --database=testing


这样就要告知 PHPUnit 在测试前要执行这个命令。这个可以在 phpunit.xml 中配置:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="MAIL_DRIVER" value="array"/>
        <env name="QUEUE_CONNECTION" value="sync"/>
        <env name="SESSION_DRIVER" value="array"/>
    </php>
</phpunit>

其中 bootstrap 那项就是执行测试函数前要引入的文件。默认引入 vendor/autoload.php

新建一个 tests/bootstrap.php 文件

<?php

require __DIR__ . '/../vendor/autoload.php';

// set up db and seed
$migrateCommand = 'php ' . realpath(__DIR__ . '/../artisan')
    . ' migrate:fresh --seed --env=testing --database=testing';
exec($migrateCommand, $output, $status);

if ($status !== 0) {
    $output = array_filter($output);
    exit(sprintf("\033[31;31m%s\033[0m" . PHP_EOL, end($output)));
}

保留原来的 autoload.php,再执行导入数据命令。然后修改 phpunit.xml,bootstrap 改成 tests/bootstrap.php

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="tests/bootstrap.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>

        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="BCRYPT_ROUNDS" value="4"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="MAIL_DRIVER" value="array"/>
    </php>
</phpunit>

 

这样就大功告成了。立即测试一下

$ vendor/bin/phpunit 
PHPUnit 7.4.3 by Sebastian Bergmann and contributors.

......                                                              6 / 6 (100%)

Time: 1.05 seconds, Memory: 12.00MB

OK (6 tests, 12 assertions)


好了,这样所有测试只做一次 migrate 和 seed ,速度是有提升的,但要特别注意,前面的测试函数中添加,修改,删除的数据是会影响到后面的测试函数的。如果这是个问题就自己取舍吧。

前一篇: guzzle 6.2 在 PHP 7.2 下及升级到 6.3 遇到的问题
后一篇: 从 PHP 7.2 升级到 PHP 7.3


添加评论

昵称:


验证码