Skip to content

Commit b98dee5

Browse files
committed
update: README
1 parent 1379d4f commit b98dee5

File tree

1 file changed

+215
-0
lines changed

1 file changed

+215
-0
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
本文主要探讨写数据库测试。
2+
3+
写 laravel 程序时,除了写生产代码,还需要写测试代码。其中,写数据库测试比较麻烦,因为需要针对每一个 test case 需要建立好数据集,该次 test case 污染的数据表还需要恢复现场,避免影响下一个 test case 运行,同时还得保证性能问题,否则随着程序不断膨胀,测试数量也越多,那每一次测试运行需要花费大量时间。
4+
5+
有两个比较好的方法可以提高数据库测试性能:
6+
7+
1. 对大量的 `tests` 按照功能分组。如有 1000 个 tests,可以按照业务功能分组,如 `group1:1-200, group2:201-800, group3: 801-1000`。这样可以 `并发运行` 每组测试包裹。
8+
2. 只恢复每个 `test case` 污染的表,而不需要把所有的数据表重新恢复,否则表数量越多测试代码执行越慢。
9+
这里聊下方法2的具体做法。
10+
11+
假设程序有50张表,每次运行测试时首先需要为每组构建好独立的对应数据库,然后创建数据表,最后就是填充测试数据(`fixtures`)。`fixtures` 可用 `yml` 格式定义,既直观也方便维护,如:
12+
13+
```
14+
#simple.yml
15+
accounts:
16+
- id: 1
17+
person_id: 2
18+
type: investment
19+
is_included: true
20+
- id: 2
21+
person_id: 2
22+
type: investment
23+
is_included: true
24+
transactions:
25+
- account_id: 1
26+
posted_date: '2017-01-01'
27+
amount: 10000
28+
transaction_category_id: 1
29+
- account_id: 2
30+
posted_date: '2017-01-02'
31+
amount: 10001
32+
transaction_category_id: 2
33+
```
34+
35+
然后需要写个 `yamlSeeder class` 来把数据集填充到临时数据库里:
36+
37+
```
38+
abstract class YamlSeeder extends \Illuminate\Database\Seeder
39+
{
40+
private $files;
41+
42+
public function __construct(array $files)
43+
{
44+
$this->files = $files
45+
}
46+
47+
public function run(array $tables = []): void
48+
{
49+
// Close unique and foreign key constraint
50+
$db = $this->container['db'];
51+
$db->statement('SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;');
52+
$db->statement('SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;');
53+
54+
foreach($this->files as $file) {
55+
...
56+
57+
// Convert yaml data to array
58+
$fixtures = \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
59+
60+
...
61+
62+
foreach($fixtures as $table => $data) {
63+
// Only seed specified tables, it is important!!!
64+
if ($tables && !in_array($table, $tables, true)) {
65+
continue;
66+
}
67+
68+
$db->table($table)->truncate();
69+
70+
if (!$db->table($table)->insert($data)) {
71+
throw new \RuntimeException('xxx');
72+
}
73+
}
74+
75+
...
76+
}
77+
78+
// Open unique and foreign key constraint
79+
$db->statement('SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;');
80+
$db->statement('SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;');
81+
}
82+
}
83+
84+
class SimpleYamlSeeder extends YamlSeeder
85+
{
86+
public function __construct()
87+
{
88+
parent::__construct([database_path('seeds/simple.yml')]);
89+
}
90+
}
91+
```
92+
93+
上面的代码有一个关键处是参数 `$tables`:如果参数是空数组,就把所有数据表数据插入随机数据库里;如果是指定的数据表,只重刷指定的数据表。这样会很大提高数据库测试的性能,因为可以在每一个 `test case` 里只需要指定本次测试所污染的数据表。在 `tests/TestCase.php` 中可以在 `setUp()` 设置数据库重装操作:
94+
95+
```
96+
abstract class TestCase extends \Illuminate\Foundation\Testing\TestCase
97+
{
98+
protected static $tablesToReseed = [];
99+
100+
public function seed($class = 'DatabaseSeeder', array $tables = []): void
101+
{
102+
$this->artisan('db:seed', ['--class' => $class, '--tables' => implode(',', $tables)]);
103+
}
104+
105+
protected function reseed(): void
106+
{
107+
// TEST_SEEDERS is defined in phpunit.xml, e.g. <env name="TEST_SEEDERS" value="\SimpleYamlSeeder"/>
108+
$seeders = env('TEST_SEEDERS') ? explode(',', env('TEST_SEEDERS')) : [];
109+
110+
if ($seeders && is_array(static::$tablesToReseed)) {
111+
foreach ($seeders as $seeder) {
112+
$this->seed($seeder, static::$tablesToReseed);
113+
}
114+
}
115+
116+
\Cache::flush();
117+
118+
static::$tablesToReseed = false;
119+
}
120+
121+
protected static function reseedInNextTest(array $tables = []): void
122+
{
123+
static::$tablesToReseed = $tables;
124+
}
125+
}
126+
```
127+
128+
这样就可以在每一个 `test case` 中定义本次污染的数据表,保证下一个 `test case` 在运行前重刷下被污染的数据表,如:
129+
130+
final class AccountControllerTest extends TestCase
131+
{
132+
...
133+
134+
public function testUpdateAccount()
135+
{
136+
static::reseedInNextTest([Account::TABLE, Transaction::TABLE]);
137+
138+
...
139+
}
140+
141+
}
142+
143+
这样会极大提高数据库测试效率,不推荐使用 Laravel 给出的 `\Illuminate\Foundation\Testing\DatabaseMigrations``\Illuminate\Foundation\Testing\DatabaseTransactions`,效率并不高。
144+
145+
laravel 的 `db:seed` 命令没有 `--tables` 这个 `options`,所以需要扩展 `\Illuminate\Database\Console\Seeds\SeedCommand`:
146+
147+
```
148+
class SeedCommand extends \Illuminate\Database\Console\Seeds\SeedCommand
149+
{
150+
public function fire()
151+
{
152+
if (!$this->confirmToProceed()) {
153+
return;
154+
}
155+
156+
$this->resolver->setDefaultConnection($this->getDatabase());
157+
158+
Model::unguarded(function () {
159+
$this->getSeeder()->run($this->getTables());
160+
});
161+
}
162+
163+
protected function getTables()
164+
{
165+
$tables = $this->input->getOption('tables');
166+
167+
return $tables ? explode(',', $tables) : [];
168+
}
169+
170+
protected function getOptions()
171+
{
172+
$options = parent::getOptions();
173+
$options[] = ['tables', null, InputOption::VALUE_OPTIONAL, 'A comma-separated list of tables to seed, all if left empty'];
174+
175+
return $options;
176+
}
177+
}
178+
```
179+
180+
当然还得写 `SeedServiceProvider()` 来覆盖原有的 `Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::registerSeedCommand()` 中注册的 `command.seed` ,然后在 `config/app.php` 中注册:
181+
182+
```
183+
class SeedServiceProvider extends ServiceProvider
184+
{
185+
/**
186+
* Indicates if loading of the provider is deferred.
187+
*
188+
* @var bool
189+
*/
190+
protected $defer = true;
191+
192+
/**
193+
* @see \Illuminate\Database\SeedServiceProvider::registerSeedCommand()
194+
*/
195+
public function register()
196+
{
197+
$this->app->singleton('command.seed', function ($app) {
198+
return new SeedCommand($app['db']);
199+
});
200+
201+
$this->commands('command.seed');
202+
}
203+
204+
public function provides()
205+
{
206+
return ['command.seed'];
207+
}
208+
}
209+
```
210+
211+
OK,这样所有的工作都做完了。。以后写数据库测试性能会提高很多,大量的 `test case` 可以在短时间内运行完毕。
212+
213+
最后,写测试代码是必须的,好处非常多,随着项目程序越来越大,就会深深感觉到写测试是必须的,一劳永逸,值得花时间投资。也是作为一名软件工程师的必备要求。
214+
215+
引用地址: [写 Laravel 测试代码 (一)](https://learnku.com/articles/5053/write-the-laravel-test-code-1)

0 commit comments

Comments
 (0)