文档章节

写Laravel测试代码(1)

botkenni
 botkenni
发布于 2017/09/03 22:37
字数 1080
阅读 10
收藏 0
点赞 0
评论 0

本文主要探讨写数据库测试。

写laravel程序时,除了写生产代码,还需要写测试代码。其中,写数据库测试比较麻烦,因为需要针对每一个test case需要建立好数据集,该次test case污染的数据表还需要恢复现场,避免影响下一个test case运行,同时还得保证性能问题,否则随着程序不断膨胀,测试数量也越多,那每一次测试运行需要花费大量时间。

有两个比较好的方法可以提高数据库测试性能:

  1. 对大量的tests按照功能分组。如有1000个tests,可以按照业务功能分组,如group1:1-200, group2:201-800, group3: 801-1000。这样可以并发运行每组测试包裹。

  2. 只恢复每个test case污染的表,而不需要把所有的数据表重新恢复,否则表数量越多测试代码执行越慢。

这里聊下方法2的具体做法。

假设程序有50张表,每次运行测试时首先需要为每组构建好独立的对应数据库,然后创建数据表,最后就是填充测试数据(fixtures)。fixtures可用yml格式定义,既直观也方便维护,如:

#simple.yml
accounts:
  - id: 1
    person_id: 2
    type: investment
    is_included: true
  - id: 2
    person_id: 2
    type: investment
    is_included: true
transactions:
  - account_id: 1
    posted_date: '2017-01-01'
    amount: 10000
    transaction_category_id: 1   
  - account_id: 2
    posted_date: '2017-01-02'
    amount: 10001
    transaction_category_id: 2

然后需要写个yamlSeeder class来把数据集填充到临时数据库里:

abstract class YamlSeeder extends \Illuminate\Database\Seeder
{
    private $files;

    public function __construct(array $files)
    {
        $this->files = $files
    }
    
    public function run(array $tables = []): void
    {
        // Close unique and foreign key constraint
        $db = $this->container['db'];
        $db->statement('SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;');
        $db->statement('SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;');
        
        foreach($this->files as $file) {
            ...
            
            // Convert yaml data to array
            $fixtures = \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
            
            ...
            
            foreach($fixtures as $table => $data) {
                // Only seed specified tables, it is important!!!
                if ($tables && !in_array($table, $tables, true)) {
                    continue;
                }
                
                $db->table($table)->truncate();

                if (!$db->table($table)->insert($data)) {
                    throw new \RuntimeException('xxx');
                }
            }
            
            ...
        }
        
        // Open unique and foreign key constraint
        $db->statement('SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;');
        $db->statement('SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;');
    }
}

class SimpleYamlSeeder extends YamlSeeder
{
    public function __construct()
    {
        parent::__construct([database.path('seeds/simple.yml')]);
    }
}

上面的代码有一个关键处是参数$tables:如果参数是空数组,就把所有数据表数据插入随机数据库里;如果是指定的数据表,只重刷指定的数据表。这样会很大提高数据库测试的性能,因为可以在每一个test case里只需要指定本次测试所污染的数据表。在tests/TestCase.php中可以在setUp()设置数据库重装操作:

    abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase
    {
        protected static $tablesToReseed = [];
        
        public function seed($class = 'DatabaseSeeder', array $tables = []): void
        {
            $this->artisan('db:seed', ['--class' => $class, '--tables' => implode(',', $tables)]);
        }
        
        protected function reseed(): void
        {
            // TEST_SEEDERS is defined in phpunit.xml, e.g. <env name="TEST_SEEDERS" value="\SimpleYamlSeeder"/>
            $seeders = env('TEST_SEEDERS') ? explode(',', env('TEST_SEEDERS')) : [];
            
            if ($seeders && is_array(static::$tablesToReseed)) {
                foreach ($seeders as $seeder) {
                    $this->seed($seeder, static::$tablesToReseed);
                }
            }
            
            \Cache::flush();
            
            static::$tablesToReseed = false;
        }
        
        protected static function reseedInNextTest(array $tables = []): void
        {
            static::$tablesToReseed = $tables;
        }
    }

这样就可以在每一个test case中定义本次污染的数据表,保证下一个test case在运行前重刷下被污染的数据表,如:

    final class AccountControllerTest extends TestCase
    {
        ...
        
        public function testUpdateAccount()
        {
            static::reseedInNextTest([Account::class, Transaction::class]);
            
            ...
        }
        
    }

这样会极大提高数据库测试效率,不推荐使用Laravel给出的\Illuminate\Foundation\Testing\DatabaseMigrations 和 \Illuminate\Foundation\Testing\DatabaseTransactions,效率并不高。

laravel的db:seed命令没有--tables这个options,所以需要扩展\Illuminate\Database\Console\Seeds\SeedCommand:

class SeedCommand extends \Illuminate\Database\Console\Seeds\SeedCommand
{
    public function fire()
    {
        if (!$this->confirmToProceed()) {
            return;
        }

        $this->resolver->setDefaultConnection($this->getDatabase());

        Model::unguarded(function () {
            $this->getSeeder()->run($this->getTables());
        });
    }
    
    protected function getTables()
    {
        $tables = $this->input->getOption('tables');

        return $tables ? explode(',', $tables) : [];
    }

    protected function getOptions()
    {
        $options   = parent::getOptions();
        $options[] = ['tables', null, InputOption::VALUE_OPTIONAL, 'A comma-separated list of tables to seed, all if left empty'];

        return $options;
    }
}

当然还得写SeedServiceProvider()来覆盖原有的Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::registerSeedCommand()中注册的command.seed,然后在config/app.php中注册:

class SeedServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = true;

    /**
     * @see \Illuminate\Database\SeedServiceProvider::registerSeedCommand()
     */
    public function register()
    {
        $this->app->singleton('command.seed', function ($app) {
            return new SeedCommand($app['db']);
        });

        $this->commands('command.seed');
    }

    public function provides()
    {
        return ['command.seed'];
    }
}

OK,这样所有的工作都做完了。。以后写数据库测试性能会提高很多,大量的test case可以在短时间内运行完毕。

最后,写测试代码是必须的,好处非常多,随着项目程序越来越大,就会深深感觉到写测试是必须的,一劳永逸,值得花时间投资。也是作为一名软件工程师的必备要求。

© 著作权归作者所有

共有 人打赏支持
botkenni
粉丝 15
博文 377
码字总数 378586
作品 0
西城
程序员
Laravel5.3之PHP反射(Reflection) (上)

说明:Laravel中经常使用PHP的反射特性来设计代码,本文主要学习PHP的反射特性,来提高写代码时的设计质量。PHP提供一套检测的两个工具包:和,类似于探针一样的东西来探测这些一等公民。本文...

botkenni
2016/11/07
29
0
谈谈我第一次如何为 Laravel 贡献源码

起源 办法总比困难多,所以发现问题比解决问题更难得到。 事情的起源是因为在调试一段模型事件时出现的,当时随便往数据库里插入了一段字符串 ,而不是标准的 。然后发现我的模型事件中查找不...

等月人
05/28
0
0
Laravel5.3之函数处理(Function Handling)

说明:Laravel中经常使用PHP的来设计代码,本文主要学习PHP的特性,来提高写代码时的设计质量。PHP提供了一些函数处理操作的内置函数,主要有: Function Handling calluserfuncarray()/callu...

botkenni
2016/11/15
5
0
技术分享 - 人生苦短, 我用 laravel

date: 2017-12-27 18:35:16 title: 技术分享 - 人生苦短, 我用 laravel 应聘 慕课网 的讲师, 选题是 laravel 相关的, 仔细斟酌之下, 定了这个选题: 人生苦短, 我用 laravel 希望这次分享, 能...

daydaygo
2017/12/28
0
0
Laravel之基于PHPStorm编辑器的Laravel开发

引言 本文主要讲述在PHPStorm编辑器中如何使用PHPStorm的Laravel插件和Laravel IDE Helper来开发Laravel程序,结合个人积累的一点经验来说明使用PHPStorm编辑器来开发程序还是很顺手的,内容...

botkenni
2016/10/10
44
0
基于Model Event模型事件的Laravel实时APP

说明:本文主要来源于real-time-apps-laravel-5-1-event-broadcasting 本文主要基于Laravel的Model Event介绍该框架的实时通信功能,Laravel模型的生命周期中包含事件:、、、、,、、、、,同...

botkenni
2016/10/11
23
0
13 个快速构建 Laravel 后台的扩展包

Laravel 后台生成器扩展包 开发者们都是懒惰的,不,我不是在开玩笑,他们努力创建系统,帮助他们在未来避免更多的、尤其是重复性的工作。现在有一些相当优秀的例子,我们不需要在编写重复的...

angkee
2017/12/06
0
0
Laravel Composer Package 开发实战 toastr-for-laravel5

本文原链接来自我的博客,地址: Laravel Composer Package 开发实战 toastr-for-laravel5 在Laravel的文档中有Package Development,对于入门开发人员来说还是比较抽象,因为开发一个包需要了...

Yuansir
2016/01/22
216
0
Laravel5.2之Validator

引言: Laravel提供了Validator模块,可解决表单提交验证等一些需求,并且可以在视图View中显示错误验证信息,交互还是很友好的。注明:作者水平有限,有错误或建议请指正,轻拍。 概述: La...

botkenni
2016/10/10
50
0
Laravel 5.1 LTS 发布,支持 PSR-2

Laravel 5.1 是 Laravel 首个 LTS 版本,包含很多新特性。 Laravel 5.1 现在开始会包括 3 年的安全修复。此版本也重新编写了文档,而且添加了实时搜索的自动完成功能。 应用和生成器转换为 ...

raykwok
2015/06/09
2.3K
9

没有更多内容

加载失败,请刷新页面

加载更多

下一页

大数据教程(2.13):keepalived+nginx(多主多活)高可用集群搭建教程【自动化脚本】

上一章节博主为大家介绍了目前大型互联网项目的keepalived+nginx(主备)高可用系统架构体系,相信大家应该看了博主的文章对keepalived/nginx技术已经有一定的了解,在本节博主将为大家分享k...

em_aaron
6分钟前
0
0
Git 2.18版本发布:支持Git协议v2,提升性能

在最新的官方 Git 客户端正式版2.18中添加了对 Git wire 协议 v2 的支持,并引入了一些性能与 UI 改进的新特性。在 Git 的核心团队成员 Brandon Williams 公开宣布这一消息前几周,Git 协议 ...

六库科技
10分钟前
0
0
Java8新特性之接口

在JDK8以前,我们定义接口类中,方法都是抽象的,并且不能存在静态方法。所有的方法命名规则基本上都是 public [返回类型] [方法名](参数params) throws [异常类型] {}。 JDK8为接口的定义带...

developlee的潇洒人生
48分钟前
0
0
aop + annotation 实现统一日志记录

aop + annotation 实现统一日志记录 在开发中,我们可能需要记录异常日志。由于异常比较分散,每个 service 方法都可能发生异常,如果我们都去做处理,会出现很多重复编码,也不好维护。这种...

长安一梦
59分钟前
2
0
将博客搬至CSDN

AHUSKY
今天
1
0
Python web框架Django学习(1)

1.Django简介 (1)Python下有许多款不同的 Web 框架。Django是重量级选手中最有代表性的一位。许多成功的网站和APP都基于Django。Django是一个开放源代码的Web应用框架,由Python写成。 (2...

十年磨一剑3344
今天
0
0
Databook-数据之书

Databook-数据之书 用于数据分析的Jupyter Notebooks。 不需购买服务器,快速开始自己的数据分析过程。 源码:https://github.com/openthings/databook 作者:openthings,https://github.co...

openthings
今天
7
0
Python PIPEs

https://www.python-course.eu/pipes.php https://www.tutorialspoint.com/python/os_pipe.htm

zungyiu
今天
1
0
gRPC学习笔记

gRPC编程流程 1. proto文件定义 proto文件用于定义需要通过gRPC生成的接口,可以理解为接口定义文档 2. 通过构建工具生成服务基类代码-Maven或Gradle 3. 服务端开发 服务端实现类须实现通过构...

OSC_fly
今天
0
0
Docker Mac (三) Dockerfile 及命令

Dockerfile 最近学习docker的时候,遇到一件怪事,关于docker镜像可能会被破坏,还不知道它会有此措施 所以需要了解构建Dockerfile的正确方法 Dockerfile是由一系列命令和参数构成的脚本,这些命...

___大侠
今天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部