寫 Laravel 測試程式碼 (二)

lx1036發表於2017-07-30

本文主要探討資料庫測試。

寫Laravel測試程式碼(一) 中聊了關於如何提高 laravel 資料庫測試效能,其實簡單一句就是:每一個test case, 只重新 seed 被汙染的表。 OK,這裡有一個前提問題:那如何構建臨時測試資料庫呢?本文主要探討如何構建臨時測試資料庫。

資料庫設計圖紙

任何一個軟體都需要資料庫設計圖紙,可以使用免費的MySqlWorkbench或者收費的Navicat Data Modler軟體。這裡使用免費的MySqlWorkbench來設計資料庫圖紙,類似下圖:

圖片描述

這裡作為範例簡單設計了5個model,當然大型程式都會有100個以上model。再利用軟體的Export SQL功能匯出資料庫的schema,這個schema檔案就作為構建臨時測試資料庫的原料,schema檔案類似如下:

圖片描述

臨時資料庫構建類

在得到 schema 檔案後,就可以寫一個臨時資料庫構建類來建立臨時測試資料庫。這裡臨時表示該測試資料庫使用完後即drop掉,且資料庫名字是隨機的,這樣可以保證同時併發進行測試。需要先在phpunit.xml中指定資料庫配置資訊:

...
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_DATABASE" value="lx1036"/>
        <env name="DB_USERNAME" value="testing"/>
        <env name="DB_PASSWORD" value="testing"/>
    </php>
</phpunit>

然後在config/database.php中寫上當執行測試時指定新構建的測試資料庫:

'mysql' => [
            'driver' => 'mysql',
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '3306'),
            'database' => env('APP_ENV') === 'testing' ? \Tests\Database::getRandomDBName(env('DB_DATABASE', 'lx1036'), env('DB_HOST', 'localhost'), env('DB_USERNAME', 'root'), env('DB_PASSWORD')) : env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'unix_socket' => env('DB_SOCKET', ''),
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix' => '',
            'strict' => true,
            'engine' => null,
        ],

然後寫一個臨時測試資料庫構建類:

<?php

namespace Tests;

use PDO;

/**
 * Singleton class to enable parallel PHPUnit processes
 *
 * 1) Generate a random testing database with automatic destroy upon finish
 * 2) Initialize the database schemas using SQL file specified by constant SQL_PATH
 * 3) Remove orphan test databases
 */
class Database
{
    /** @var  \Tests\Database singleton to drop test database in destructor */
    protected static $instance;

    /** @var string */
    protected static $db_name;

    /** @var string */
    protected static $host;

    /** @var string */
    protected static $username;

    /** @var string */
    protected static $password;

    public function __construct(string $db_name)
    {
        static::$db_name = $db_name;
    }

    public function __destruct()
    {
        if (static::$db_name) {
            $pdo = new PDO('mysql:host=' . static::$host . ';' . 'dbname=' . static::$db_name, static::$username, static::$password);
            $pdo->exec('DROP DATABASE `' . static::$db_name . '`');
        }
    }

    public static function getRandomDBName(string $prefix, string $host, string $username, string $password, string $charset = 'utf8mb4', string $collation = 'utf8mb4_unicode_ci'): string
    {
        if (static::$instance) {
            return static::$instance->getDBName();
        }

        $db_name = $prefix . '_' . date('ymd') . '_' . str_random();

        $pdo = new PDO('mysql:host=' . $host, $username, $password);

        // Remove orphan database
        static::removeOrphans($pdo, $prefix);

        // Create random database
        $pdo->exec('CREATE DATABASE `' . $db_name . '` DEFAULT CHARACTER SET ' . $charset . ' COLLATE ' . $collation);
        $pdo->exec('USE `' . $db_name . '`');

        // Create tables in specified random database
        $schema_file = __DIR__ . '/../database/seeds/mysql.sql';

        if ($pdo->exec(file_get_contents($schema_file)) === false) {
            throw new \ErrorException("Cannot create tables by sql file: " . $schema_file . ' because of ' . $pdo->errorInfo()[2]);
        }

        /*
        // Check if tables are inserted.
        $result = $pdo->query("SHOW TABLES")->fetchAll(PDO::FETCH_NUM);
        dump($result);*/

        static::$instance = new static($db_name);
        static::$host     = $host;
        static::$username = $username;
        static::$password = $password;

        dump($db_name);
        return $db_name;
    }

    /**
     * Remove orphan database if exists.
     *
     * @param PDO $pdo
     * @param string $prefix
     */
    public static function removeOrphans(PDO $pdo, string $prefix)
    {
        $databases = $pdo->query('SHOW DATABASES LIKE "' . $prefix . '%"')->fetchAll();

        foreach ($databases as $database) {
            $database = reset($database);

            if (starts_with($database, $prefix) && is_numeric(explode('_', $database)[1])) {
                $pdo->exec('DROP DATABASE `' . $database . '`');

                echo 'Drop database ' . $database . PHP_EOL;
            }
        }
    }

    /**
     * @return string
     */
    public static function getDBName(): string
    {
        return static::$db_name;
    }

    /**
     * @return string
     */
    public static function getHost(): string
    {
        return static::$host;
    }

    /**
     * @return string
     */
    public static function getUsername(): string
    {
        return static::$username;
    }

    /**
     * @return string
     */
    public static function getPassword(): string
    {
        return static::$password;
    }
}

這樣,當執行測試時連線的就是臨時構建的測試資料庫,測試執行完畢就drop掉資料庫,並且可以同時開多個視窗(執行緒)來分組執行test cases。最後還得在mysql localhost中建立testing@testing使用者並授權,以root使用者登入local mysql

CREATE USER 'testing'@'localhost' IDENTIFIED BY 'testing';
GRANT ALL ON `lx1036%`.* TO 'testing'@'localhost';

這樣就臨時測試資料庫就準備完畢了,然後就是seed 測試資料,執行unit/feature tests, 執行assert等等,可以參考寫Laravel測試程式碼(一)。這裡執行phpunit時得到的臨時測試資料庫是:

圖片描述

OK,後續再聊執行unit/feature tests時一些實踐技巧。

RightCapital招聘Laravel DevOps

相關文章