前阵子看了点Laravel源码,越看越乱,网上大部分中文文档都是直译,比较生涩难懂,还是决定看英文文档顺便就我的理解做下翻译整理记录下来
简介
Laravel构建的时候就带上测试。实际上,内含支持PHPUnit的测试,开箱即用,同时已经为你应用创建了phpunit.xml
文件。框架给你提供了方便的帮助方法,可以让你形象地测试你应用。
tests
文件夹 提供了 ExampleText.php
文件。安装完Laravel应用,只要在命令行执行vendor/bin
下的 phpunit
就能运行你的测试
测试环境
当你运行测试,Laravel会自动为测试配置环境。测试的时候Laravel自动配置session和缓存到你的数组驱动,意味着测试的时候不会持久化session和缓存数据。
需要的话,你可以自由的创建其他测试环境。测试环境的参数可以在phpunit.xml
文件里配置,但要在运行测试前确保你用config:clear
Artisan命令清理的配置缓存。
定义&运行测试
用make:test
Artisan命令创建一个新的测试案例:
php artisan make:test UserTest
这个命令会在tests
文件夹下创建一个新的UserTest
。然后你就可以像平时用PHPUnit一样定义你的测试方法。只要执行phpunit
命令就可以运行测试:
assertTrue(true);
}
}
注意:如果在测试类定义你自己的
setUp
方法,确保使用parent:setUp
应用测试
Laravel提供了非常流畅的API,可以让你向引用发送Http请求,检测输出,甚至填充表单。
例如,看下ExamleTest.php
:
visit('/')
->see('Laravel 5')
->dontSee('Rails');
}
}
visit
方法向应用发送了一个Get
请求。see
方法确保在返回的消息中可以看到给定文本。dontSee
方法确保返回消息中没有给定文本。这是Laravel可用的最基本的应用测试。
和应用交互
当然,比起确保文本出现在给定回复,你可以做更多。让我们来看些点击链接和填充表单的例子:
点击链接
在这个测试中,我们将对应用发起请求,在返回的响应中“点击”链接,然后确保登入指定URL。例如,我们假设在返回的响应 有一个文本为“About Us”的链接:
About Us
现在,让我们来写一个测试点击链接确保用户打开正确的页面:
public function testBasicExample()
{
$this->visit('/')
->click('About Us')
->seePageIs('/about-us');
}
处理表单
Laravel也为测试表单提供了多重方法。type
,select
,check
,attack
,和press
方法允许你和所有的表单输入框做交互。例如,让我们想象一下应用的注册页面上有这样一个表单:
我们可以编写一个测试来填充表单来检查结果:
public function testNewUserRegistration()
{
$this->visit('/register')
->type('Taylor', 'name')
->check('terms')
->press('Register')
->seePageIs('/dashboard');
}
当然如果表单包含其他输入比如单选按钮和下拉菜单,你也可以轻松的填充这些字段类型。这里列出每个表单的操作方法:
方法 | 描述 |
---|---|
$this->type($text, $elementName) |
输入文本 |
$this->select($value, $elementName) |
选择单选按钮和下拉框 |
$this->check($elementName) |
多选框选择 |
$this->uncheck($elementName) |
多选框取消选择 |
$this->attach($pathToFile, $elementName) |
添加附件 |
$this->press($buttonTextOrElementName) |
点击按钮 |
处理附件
如果表单包含file
输入类型,你可以用attach
方法关联
public function testPhotoCanBeUploaded()
{
$this->visit('/upload')
->type('File Name', 'name')
->attach($absolutePathToFile, 'photo')
->press('Upload')
->see('Upload Successful!');
}
测试 JSON API
Laravel也为测试JSON API和它们的响应提供了许多帮助。例如,get
,post
,put
,patch
和delete
方法用来解决各种HTTP请求。你也可以轻松用这些方法传递数据和头文件。首先,让我们写一个测试,对/user
发送一个POST
请求然后确保给定的数组在返回的Json格式中:
json('POST', '/user', ['name' => 'Sally'])
->seeJson([
'created' => true,
]);
}
}
seeJson
方法把给定数组转化成JSON,然后核实这个JSON片段是否在返回的JSON响应中出现。所以,就算返回的JSON里有其他的属性,只要给定片段存在依然可以通过测试。
核实JSON精准匹配
如果你像核实给定数组完全匹配应用返回的JSON,你可以用seeJsonEqual
方法:
json('POST', '/user', ['name' => 'Sally'])
->seeJsonEquals([
'created' => true,
]);
}
}
验证JSON结构匹配
你可以验证返回JSON是否特定结构。为此,你可以使用seeJsonStructure
方法同时传递一系列嵌套关键字:
get('/user/1')
->seeJsonStructure([
'name',
'pet' => [
'name', 'age'
]
]);
}
}
上面的例子期望收到一个name
和一个包含name
和age
的嵌套对象pet
。只要额外关键字带响应中存在seeJsonStructure
就不会失败。比如,就算pet
有一个weight
属性测试也会通过。
你可以使用*
来确保返回的JSON结构里每一个列表项都至少包含设置的属性:
get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email'
]
]);
}
}
你可以在嵌套中使用*
,这样的话,你可以确保每个用户的数据里都包含给定的属性集同是每个pet
属性都包含给定属性集:
$this->get('/users')
->seeJsonStructure([
'*' => [
'id', 'name', 'email', 'pets' => [
'*' => [
'name', 'age'
]
]
]
]);
Session/认证
Laravel为测试期间用session提供了一些帮助。首先,你可以用withSession
方法把session
的数据设置成指定数组。这个方法对请求前加载session很有用:
withSession(['foo' => 'bar'])
->visit('/');
}
}
当然,seesion常见的应用就是保存用户状态,比如认证用户。actingAs
方法提供了一个认证给定用户为当前用户的简便方法。比如,我们可以用model factory
生成和认证一个用户:
create();
$this->actingAs($user)
->withSession(['foo' => 'bar'])
->visit('/')
->see('Hello, '.$user->name);
}
}
你也可以指定用哪个guard
来认证给定用户,通过给actingAs
方法的第二个参数传递一个guard
名字:
$this->actingAs($user, 'backend')
中间件不可用
在测试你的应用的时候,你会发现你可以很方便的在某些测试中让中间件不可用。这将使你可以在隔离中间件的情况下测试你的路由和控制器。Laravel包含一个WithoutMiddlware
trait,你可以用他自动让所有的中间件不可用。
如果你只想对某些测试方法无效化中间件,你可以在测试方法中调用withoutMiddleware
方法:
withoutMiddleware();
$this->visit('/')
->see('Laravel 5');
}
}
自定义HTTP请求
如果你像发送自定义HTTP请求来获取完整的Illuminate\Http\Response
对象,你可以使用call
方法:
public function testApplication()
{
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
}
如果你创建一个POST
,PUT
或者PATCH
请求,你可能需要传送一个输入数据的数组。当然,通过Request实例
这些数据将在你的路由和控制器内可用:
$response = $this->call('POST', '/user', ['name' => 'Taylor']);
PHPUnit 断言
Laravel 为PHPUnit
提供了多个额外的断言方法:
方法 | 描述 |
---|---|
->assertResponseOk(); |
确认客户端响应一个OK状态 |
->assertResponseStatus($code); |
确认客户端响应一个指定代码 |
->assertViewHas($key, $value = null); |
确认客户端响应视图里有给定绑定数据片段 |
->assertViewHasAll(array $bindings); |
确认客户端响应视图有给定绑定数据队列 |
->assertViewMissing($key); |
确认客户端响应视图缺失一对绑定数据 |
->assertRedirectedTo($uri, $with = []); |
确认客户端是否重定向到指定URL |
->assertRedirectedToRoute($name, $parameters = [], $with = []); |
确认客户端是否重定向到指定路由 |
->assertRedirectedToAction($name, $parameters = [], $with = []); |
确认客户端是否重定向到指定动作 |
->assertSessionHas($key, $value = null); |
确认Session里有给定值 |
->assertSessionHasAll(array $bindings); |
确认Session中有给定值集合 |
->assertSessionHasErrors($bindings = [], $format = null); |
确认Session有错误绑定 |
->assertHasOldInput(); |
确认Session中有旧的输入 |
->assertSessionMissing($key); |
确认Session中缺失指定关键字 |
数据库操作
Laravel提供了各种各样有用的工具来简化测试我们的数据库驱动应用。首先,你可以用seeInDatabase
来确认数据库里是存在符合你条件的数据。例如,如果你像验证在user
表中有条数据它email
值为[email protected]
,你可以这样写:
public function testDatabase()
{
// Make call to application...
$this->seeInDatabase('users', ['email' => '[email protected]']);
}
当然,seeInDatabase
方法和其他类似的帮助方法都是为了方便。你可以自由使用任何PHPUnit的内建断言方法来支持你的测试。
测试后重置数据库
在测试后重置数据库是很有用的,这让后续测试不会受到前面测试的数据影响。
使用Migration
一个选择就是每次测试完都回滚数据库然后在下次测试前移植过去。Laravel提供了DatabaseMigrations
trait来自动进行这些操作:
visit('/')
->see('Laravel 5');
}
}
使用会话
另一个选择就是把每个测试案列包括在一个数据库会话中。同样的,Laravel提供了一个方便的DatabaseTransactions
trait来自动操作这些:
visit('/')
->see('Laravel 5');
}
}
注意:这个trait只是把默认的数据库连接包裹在会话中
模块工厂
在指定测试前,一般都需要插入一些记录到数据库中。Laravel允许使用"工厂"为你所有的Eloquent models
定义一个属性集合,你就不用在你创建测试数据的时候手动指定每一列的值了。首先,让我们看一下database/factories/ModelFactory.php
,开箱即用,这个文件包含一个工程定义:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
在factory定义的闭包中,你可以返回模块上的所有属性的测试值。这个闭包会接受一个Faker PHP Library
实例,它允许你很方便的生成各种随机的数据来测试。
当然,你可以自由地在ModelFactory.php
中添加自己的额外工厂。你也可以为每个model添加另外的工厂文件来更好的组织。比如,你可以在你的database/factories
目录下创建UserFactory.php
和CommentFactory.php
文件。
多样工厂类型
有时候你像为同一个Eloquent model类创建多个工程。比如除了普通用户你还想为管理员用户添加工程。你可以用defineAs
方法定义这些工程:
$factory->defineAs(App\User::class, 'admin', function ($faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => str_random(10),
'remember_token' => str_random(10),
'admin' => true,
];
});
如果不想从基础用户工厂中复制所有属性,你可以用raw
方法来获取基类的所有属性。然后你只要任何你要加的值添加进去就可以了:
$factory->defineAs(App\User::class, 'admin', function ($faker) use ($factory) {
$user = $factory->raw(App\User::class);
return array_merge($user, ['admin' => true]);
});
在测试中用工厂
当你定义好工厂,你可以用factory
方法在你的测试或数据库seed文件中使用它们来生成model实例。让我们看一些创建model的例子。首先,我们用make
方法,它会创建model但不会存入数据库:
public function testDatabase()
{
$user = factory(App\User::class)->make();
// Use model in tests...
}
如果你想重写一些你model中的默认值,你可以给make
方法传递一个数组。只有指定的值会被替换,其他的值都会保留你在工厂中定义的默认值:
$user = factory(App\User::class)->make([
'name' => 'Abigail',
]);
你也可以创建一个model集合或者创建一个给定类型的model:
// Create three App\User instances...
$users = factory(App\User::class, 3)->make();
// Create an App\User "admin" instance...
$user = factory(App\User::class, 'admin')->make();
// Create three App\User "admin" instances...
$users = factory(App\User::class, 'admin', 3)->make();
持久化工厂model
create
方法不但创建model实例,还会用Eloquent的save
方法把它们存入数据库:
public function testDatabase()
{
$user = factory(App\User::class)->create();
// Use model in tests...
}
同样的,你可以通过给create
方法传数组来重写model属性:
$user = factory(App\User::class)->create([
'name' => 'Abigail',
]);
为Model添加关联
你或许想保存多个model到数据库。在这个例子中,你可以对一个已建model附加关联。当你用create
方法创建多个models,会返回一个Eloquent collection
实例,它让你可以用它提供的任何快捷函数,比如each
:
$users = factory(App\User::class, 3)
->create()
->each(function ($u) {
$u->posts()->save(factory(App\Post::class)->make());
});
关联和属性闭包
你可以在工厂定义中用属性闭包添加关联。比如,如果你想在创建Post
的时候创建一个User
,你可以这样做:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
}
];
});
这些闭包还能接收工厂的属性数组:
$factory->define(App\Post::class, function ($faker) {
return [
'title' => $faker->title,
'content' => $faker->paragraph,
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'user_type' => function (array $post) {
return App\User::find($post['user_id'])->type;
}
];
});
模仿
模仿事件
如果你在大量使用Laravel的事件系统,你可能希望在测试中有一个让事件安静下来或者模拟它。比如,如果你在测试用户注册,你可能不希望所有的UserRegistered
事件操作被执行,因为它们会发送一个welcome
电子邮件等等。
Laravel提供了一个expectsEvents
方法来验证预计的时间被执行,同时阻止任何这些时间的操作被执行:
expectsEvents(App\Events\UserRegistered::class);
// Test user registration...
}
}
你可以用doesntExpectEvents
方法来验证给定事件没有被触发:
expectsEvents(App\Events\PodcastWasPurchased::class);
$this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);
// Test purchasing podcast...
}
}
如果你像阻止所有的事件操作,你可以用withoutEvents
方法:
withoutEvents();
// Test user registration code...
}
}
模拟工作
有时候,当你发送请求给应用的时候你想测试特定工作是否被控制器分派下来。这允许你隔离工作逻辑来测试路由或者控制器。当然,你可以在独立的测试类里测试工作。
Laravel提供了expectsJobs
方法来验证预期的工作有没有分派下来,但不会执行工作:
expectsJobs(App\Jobs\PurchasePodcast::class);
// Test purchase podcast code...
}
}
注意:这个方法只会检索通过
DispatchesJobs
trait或者dispatch
帮助函数分派下来的方法。它不会检索由Queue::push
直接发送下来的工作。
模拟门面
在测试中,你可能经常希望模拟调用一个Laravel门面,例如,看一下下面的控制器动作:
你可以使用shouldReceive
方法模拟调用Cache
门面,它会返回一个Mockery
实例。由于门面实际上由Laravel的服务容器处理和管理的,它们会比典型的静态类更容易测试。例如,让我们模拟条用Cache
门面:
once()
->with('key')
->andReturn('value');
$this->visit('/users')->see('value');
}
}
注意:在你运行测试的时候,不要去模拟
Request
门面,你应该把你想要的输入传入HTTP帮助方法比如call
和post