【Laravel5.2翻译】单元测试


前阵子看了点Laravel源码,越看越乱,网上大部分中文文档都是直译,比较生涩难懂,还是决定看英文文档顺便就我的理解做下翻译整理记录下来

思维导图

简介

Laravel构建的时候就带上测试。实际上,内含支持PHPUnit的测试,开箱即用,同时已经为你应用创建了phpunit.xml文件。框架给你提供了方便的帮助方法,可以让你形象地测试你应用。

tests文件夹 提供了 ExampleText.php 文件。安装完Laravel应用,只要在命令行执行vendor/bin下的 phpunit就能运行你的测试

测试环境

当你运行测试,Laravel会自动为测试配置环境。测试的时候Laravel自动配置session和缓存到你的数组驱动,意味着测试的时候不会持久化session和缓存数据。
需要的话,你可以自由的创建其他测试环境。测试环境的参数可以在phpunit.xml文件里配置,但要在运行测试前确保你用config:clear Artisan命令清理的配置缓存。

定义&运行测试

make:testArtisan命令创建一个新的测试案例:

php artisan make:test UserTest

这个命令会在tests文件夹下创建一个新的UserTest。然后你就可以像平时用PHPUnit一样定义你的测试方法。只要执行phpunit命令就可以运行测试:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class UserTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testExample()
    {
        $this->assertTrue(true);
    }
}

注意:如果在测试类定义你自己的setUp方法,确保使用parent:setUp

应用测试

Laravel提供了非常流畅的API,可以让你向引用发送Http请求,检测输出,甚至填充表单。
例如,看下ExamleTest.php

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->visit('/')
             ->see('Laravel 5')
             ->dontSee('Rails');
    }
}

visit方法向应用发送了一个Get请求。see方法确保在返回的消息中可以看到给定文本。dontSee方法确保返回消息中没有给定文本。这是Laravel可用的最基本的应用测试。

和应用交互

当然,比起确保文本出现在给定回复,你可以做更多。让我们来看些点击链接和填充表单的例子:

点击链接
在这个测试中,我们将对应用发起请求,在返回的响应中“点击”链接,然后确保登入指定URL。例如,我们假设在返回的响应 有一个文本为“About Us”的链接:

<a href = "/about-us"> About Us</a>

现在,让我们来写一个测试点击链接确保用户打开正确的页面:

public function testBasicExample()
{
    $this->visit('/')
         ->click('About Us')
         ->seePageIs('/about-us');
}

处理表单

Laravel也为测试表单提供了多重方法。type,select,check,attack,和press方法允许你和所有的表单输入框做交互。例如,让我们想象一下应用的注册页面上有这样一个表单:

<form action="/register" method="POST">
    {{ csrf_field() }}

    <div>
        Name: <input type="text" name="name">
    </div>

    <div>
        <input type="checkbox" value="yes" name="terms"> Accept Terms
    </div>

    <div>
        <input type="submit" value="Register">
    </div>
</form>

我们可以编写一个测试来填充表单来检查结果:

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,patchdelete方法用来解决各种HTTP请求。你也可以轻松用这些方法传递数据和头文件。首先,让我们写一个测试,对/user发送一个POST请求然后确保给定的数组在返回的Json格式中:

<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->json('POST', '/user', ['name' => 'Sally'])
             ->seeJson([
                 'created' => true,
             ]);
    }
}

seeJson方法把给定数组转化成JSON,然后核实这个JSON片段是否在返回的JSON响应中出现。所以,就算返回的JSON里有其他的属性,只要给定片段存在依然可以通过测试。

核实JSON精准匹配
如果你像核实给定数组完全匹配应用返回的JSON,你可以用seeJsonEqual方法:

<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->json('POST', '/user', ['name' => 'Sally'])
             ->seeJsonEquals([
                 'created' => true,
             ]);
    }
}

验证JSON结构匹配
你可以验证返回JSON是否特定结构。为此,你可以使用seeJsonStructure方法同时传递一系列嵌套关键字:

<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->get('/user/1')
             ->seeJsonStructure([
                 'name',
                 'pet' => [
                     'name', 'age'
                 ]
             ]);
    }
}

上面的例子期望收到一个name和一个包含nameage的嵌套对象pet。只要额外关键字带响应中存在seeJsonStructure就不会失败。比如,就算pet有一个weight属性测试也会通过。

你可以使用*来确保返回的JSON结构里每一个列表项都至少包含设置的属性:

<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        // Assert that each user in the list has at least an id, name and email attribute.
        $this->get('/users')
             ->seeJsonStructure([
                 '*' => [
                     'id', 'name', 'email'
                 ]
             ]);
    }
}

你可以在嵌套中使用*,这样的话,你可以确保每个用户的数据里都包含给定的属性集同是每个pet属性都包含给定属性集:

$this->get('/users')
     ->seeJsonStructure([
         '*' => [
             'id', 'name', 'email', 'pets' => [
                 '*' => [
                     'name', 'age'
                 ]
             ]
         ]
     ]);

Session/认证

Laravel为测试期间用session提供了一些帮助。首先,你可以用withSession方法把session的数据设置成指定数组。这个方法对请求前加载session很有用:

<?php

class ExampleTest extends TestCase
{
    public function testApplication()
    {
        $this->withSession(['foo' => 'bar'])
             ->visit('/');
    }
}

当然,seesion常见的应用就是保存用户状态,比如认证用户。actingAs方法提供了一个认证给定用户为当前用户的简便方法。比如,我们可以用model factory生成和认证一个用户:

<?php

class ExampleTest extends TestCase
{
    public function testApplication()
    {
        $user = factory(App\User::class)->create();

        $this->actingAs($user)
             ->withSession(['foo' => 'bar'])
             ->visit('/')
             ->see('Hello, '.$user->name);
    }
}

你也可以指定用哪个guard来认证给定用户,通过给actingAs方法的第二个参数传递一个guard名字:

$this->actingAs($user, 'backend')

中间件不可用

在测试你的应用的时候,你会发现你可以很方便的在某些测试中让中间件不可用。这将使你可以在隔离中间件的情况下测试你的路由和控制器。Laravel包含一个WithoutMiddlware trait,你可以用他自动让所有的中间件不可用。

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use WithoutMiddleware;

    //
}

如果你只想对某些测试方法无效化中间件,你可以在测试方法中调用withoutMiddleware方法:

<?php

class ExampleTest extends TestCase
{
    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->withoutMiddleware();

        $this->visit('/')
             ->see('Laravel 5');
    }
}

自定义HTTP请求

如果你像发送自定义HTTP请求来获取完整的Illuminate\Http\Response对象,你可以使用call方法:

public function testApplication()
{
    $response = $this->call('GET', '/');

    $this->assertEquals(200, $response->status());
}

如果你创建一个POSTPUT或者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值为sally@example.com,你可以这样写:

 public function testDatabase()
{
    // Make call to application...

    $this->seeInDatabase('users', ['email' => 'sally@example.com']);
}

当然,seeInDatabase方法和其他类似的帮助方法都是为了方便。你可以自由使用任何PHPUnit的内建断言方法来支持你的测试。

测试后重置数据库

在测试后重置数据库是很有用的,这让后续测试不会受到前面测试的数据影响。

使用Migration

一个选择就是每次测试完都回滚数据库然后在下次测试前移植过去。Laravel提供了DatabaseMigrationstrait来自动进行这些操作:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use DatabaseMigrations;

    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->visit('/')
             ->see('Laravel 5');
    }
}

使用会话
另一个选择就是把每个测试案列包括在一个数据库会话中。同样的,Laravel提供了一个方便的DatabaseTransactionstrait来自动操作这些:

<?php

use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;

class ExampleTest extends TestCase
{
    use DatabaseTransactions;

    /**
     * A basic functional test example.
     *
     * @return void
     */
    public function testBasicExample()
    {
        $this->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.phpCommentFactory.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方法来验证预计的时间被执行,同时阻止任何这些时间的操作被执行:

<?php

class ExampleTest extends TestCase
{
    public function testUserRegistration()
    {
        $this->expectsEvents(App\Events\UserRegistered::class);

        // Test user registration...
    }
}

你可以用doesntExpectEvents方法来验证给定事件没有被触发:

<?php

class ExampleTest extends TestCase
{
    public function testPodcastPurchase()
    {
        $this->expectsEvents(App\Events\PodcastWasPurchased::class);

        $this->doesntExpectEvents(App\Events\PaymentWasDeclined::class);

        // Test purchasing podcast...
    }
}

如果你像阻止所有的事件操作,你可以用withoutEvents方法:

<?php

class ExampleTest extends TestCase
{
    public function testUserRegistration()
    {
        $this->withoutEvents();

        // Test user registration code...
    }
}

模拟工作

有时候,当你发送请求给应用的时候你想测试特定工作是否被控制器分派下来。这允许你隔离工作逻辑来测试路由或者控制器。当然,你可以在独立的测试类里测试工作。

Laravel提供了expectsJobs方法来验证预期的工作有没有分派下来,但不会执行工作:

<?php

class ExampleTest extends TestCase
{
    public function testPurchasePodcast()
    {
        $this->expectsJobs(App\Jobs\PurchasePodcast::class);

        // Test purchase podcast code...
    }
}

注意:这个方法只会检索通过DispatchesJobs trait或者dispatch帮助函数分派下来的方法。它不会检索由Queue::push直接发送下来的工作。

模拟门面

在测试中,你可能经常希望模拟调用一个Laravel门面,例如,看一下下面的控制器动作:

<?php

namespace App\Http\Controllers;

use Cache;

class UserController extends Controller
{
    /**
     * Show a list of all users of the application.
     *
     * @return Response
     */
    public function index()
    {
        $value = Cache::get('key');

        //
    }
}

你可以使用shouldReceive方法模拟调用Cache门面,它会返回一个Mockery实例。由于门面实际上由Laravel的服务容器处理和管理的,它们会比典型的静态类更容易测试。例如,让我们模拟条用Cache门面:

<?php

class FooTest extends TestCase
{
    public function testGetIndex()
    {
        Cache::shouldReceive('get')
                    ->once()
                    ->with('key')
                    ->andReturn('value');

        $this->visit('/users')->see('value');
    }
}

注意:在你运行测试的时候,不要去模拟Request门面,你应该把你想要的输入传入HTTP帮助方法比如callpost

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容