Laravel Dusk 前世今生(Laravel 5.4 新兵参上)

如果你在 Twitter 上关注过 Laravel 的开发者,或者如果你听过 Laravel Podcast,那你就应该知道在 Laravel 应用测试中出现的新兵 —— Laravel Dusk。 

Laravel 应用测试的背景#

首先,简单回顾一下:虽然每个人在讨论测试时使用的语言有点不同,但都一致认为单元测试是负责测试隔离开来的小块的代码 ,例如在一个类中的一个方法。而应用测试,与集成测试类似,用来测试整个应用程序作为一个整体。

自从 Jeffrey Way 的 Integrated 成为 Laravel 5.1 核心,我们便获得了 ->visit()->get()->see() 等方法去描述一个浏览器访问网站的行动。而这确实改变了我们编写的应用程序测试的能力,简单地写个例子:

/** @test */
public function cta_link_functions()
{
    $this->visit('/sales-page')
        ->click('Try it now!')
        ->see('Sign up for trial')
        ->onPage('trial-signup');
}

对后台而言,它就等于一个 PHP 的启动请求,通过我们的应用程序传递这个请求,去抓取 DOM,然后提出更多的请求,直到整条代码链完成。 从而做到在不需要浏览器的情况下去模拟浏览器的行为。

传统测试方式带来的问题#

但如果应用程序的功能依赖了 JavaScript,那该怎么办?毕竟这不是一个真正的浏览器,难以要求它去注意到你的 JavaScript。

随着时间的推移,在 Laravel 应用程序中使用和测试 JavaScript 组件的愿望不断增多,对于 Laravel 提供的工具无法测试大部分应用程序的不满也越来越多。

解决方案:Laravel Dusk#

Taylor 用 Dusk 完全重写了 Laravel 应用程序测试的工作原理。而这一切全部是基于一个名为 ChromeDriver 的工具,一个实际控制着 Chrome/Chromium 的独立的服务器。编写应用程序测试时,Dusk 会将命令发送到 ChromeDriver,ChromeDriver 随后启动 Chrome 以在浏览器中运行测试,最后返回测试结果。

Laravel 所有的非应用程序测试 —— 单元测试以及基于 HTTP 请求的测试,如 $this->get() 的代码的使用方法跟原来一样。但功能更高级,如 $this->visit() ,只是你需要多做些配置。这取决于你拉入应用程序测试包。可以拉入 Dusk composer require laravel/dusk --dev 来玩,也可以拉入 5.4 之前的测试包 composer require laravel/browser-kit-testing --dev 继续之前的测试。

注意:如果引入 Browser Kit 测试,则需要修改 TestCase 来扩展 Laravel\BrowserKitTesting\TestCase,而不是 Illuminate\Foundation\Testing\TestCase。 具体查看 Adam Wathan 的 为 Laravel 5.4 升级测试套件

入我 Dusk 门#

一旦把 Dusk 引入到应用程序中,就需要注册服务提供者。 你可以将它添加到 config/app.php 中的服务提供者列表。但实际上这种做法并不安全。为了方便 Dusk 进行测试,打开了许多生产环境上不需要的设置。因此改为在 app/Providers/AppServiceProvider.php 的 register 方法中有条件地注册会是个不错的选择,如下所示:

// AppServiceProvider
use Laravel\Dusk\DuskServiceProvider;

...

public function register()
{
    if ($this->app->environment('local', 'testing')) {
        $this->app->register(DuskServiceProvider::class);
    }
}

好了,现在我们要来安装 Dusk 了,执行下面的语句之后会创建一个 tests/Browser 目录。

php artisan dusk:install

你可能从来没有在 .env 文件中使用 APP_URL 键,因为通常对许多应用程序来说,它不是必须的,但,现在需要设置它了,因为 Dusk 依赖它来访问应用程序。 这将是一个实际可访问的 URL,因为我们正在使用一个真正的浏览器。

我们现在使用 php artisan dusk 运行测试,它可以接受 PHPUnit 可以使用的任何参数,例如,php artisan dusk --filter=the_big_button_works

写我们第一个 Dusk 测试#

像平时写应用程序测试一样来写第一个 Dusk 测试,测试通过点击按钮去到指定的位置。先运行下面的指令创建我们要用来写测试的文件:

php artisan dusk:make BigButtonTest

接下来打开 tests/Browser/BigButtonTest.php,下面是默认生成的:

<?php

namespace Tests\Browser;

use Tests\DuskTestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class BigButtonTest extends DuskTestCase
{
    /**
     * A Dusk test example.
     *
     * @return void
     */
    public function testExample()
    {
        $this->browse(function ($browser) {
            $browser->visit('/')
                    ->assertSee('Laravel');
        });
    }
}

你会注意到一些不同于我们习惯的东西。

首先,这个测试文件有命名空间!实际上是在 5.4 无论你是否使用 Dusk,默认情况下,都会生成两个为测试准备的目录:Tests\UnitTests\Feature

第二,DatabaseTransactionsWithoutMiddleware 不会默认导入。

第三,我们不再直接调用 $this->visit。 我们现在在 browse() 闭包函数中执行所有的测试,这个函数封装了 Dusk。

第四,一些可用的方法已被重命名为与其作用更一致。例如,see() 现在是 assertSee()

然后运行命令,开始测试: php artisan dusk

file

看到没看到没?是不是好赞!

还可以进一步试试:

$this->browse(function ($browser) {
    $browser->visit('/sales-page')
        ->clickLink('Try it now!')
        ->assertSee('Sign up for trial')
        ->assertPathIs('/trial-signup');
});

上面这些是我们曾经能够做到的,接下来看看新的东西。

新的交互#

有很多新的功能和新的交互方式,更多的东西还是看 文档 比较好些。 但是这里有一些与过去有所不同的地方,进一步了解多一些也无碍。

获取和设置表单上的值#

获取或设置 value,并获取 text 或页面上任何给定其 jQuery 样式选择器元素的属性。

// Get or set
$inputValue = $browser->value('#name-input');
$browser->value('#email-input', 'matt@matt.com');

// Get
$welcomeDivValue = $browser->value('.welcome-text');
$buttonDataTarget = $browser->attribute('.button', 'data-target');

与表单和其他页面元素进行交互#

与表单交互非常类似于之前的情况。 首先,选择字段的名称,或者一个 jQuery 样式选择器。

$browser->type('email', 'matt@matt.com');
$browser->type('#name-input', 'matt');

可以清除任何值:

$browser->clear('password');

可以选择下拉列表值:

$browser->select('plan', 'premium');

可以选中复选框或单选按钮:

$browser->check('agree');
$browser->uncheck('mailing-list');
$browser->radio('referred-by', 'friend');

也可以附加文件:

$browser->attach('profile-picture', __DIR__ . '/photos/user.jpg');

甚至可以执行更复杂的基于键盘和鼠标的交互:

// type 'hype' while holding the command key
$browser->keys('#magic-box', ['{command}', 'hype']);

$browser->click('#randomize');

$browser->mouseover('.hover-me');

$browser->drag('#tag__awesome', '.approved-tags');

最后,我们可以将我们的任何操作限定为我们正在处理的网站的特定形式或部分:

$browser->with('.sign-up-form', function ($form) {
    $form->type('name', 'Jim')
        ->clickLink('Go');
});

等待#

这可能是 Dusk 中最对外的概念。 因为一个真正的浏览器,它实际上必须加载页面上的所有外部资源,而这意味着那些资源可能还没能那么快加载完。

有几个方法可以帮助你解决这个问题。 首先,您可以手动暂停测试:

// Pause for 500ms
$browser->pause(500);

或者,等待(默认情况下,最多 5 秒),直到给定的元素出现或消失:

$browser->waitFor('.chat-box');

// wait a maximum of 2 seconds for the chat box to appear
$browser->waitFor('.chat-box', 2);

$browser->waitUntilMissing('.loading');
$browser->waitForText('You have arrived!');
$browser->waitForLink('Proceed');

// wait and scope
$browser->whenAvailable('.chat-box'), function ($chatBox) {
    $chatBox->assertSee('What is your message?')
        ->type('message', 'Hello!')
        ->press('Send');
});

// wait until JavaScript expression returns true
$browser->waitUntil('App.initialized');

创建多个浏览器#

从文档中举一个例子,如果你想测试一个基于 websocket 对话框的工作情况,只需使用两个单独的浏览器进行会话:

$this->browse(function ($first, $second) {
    $first->loginAs(User::find(1))
          ->visit('/home')
          ->waitForText('Message');

    $second->loginAs(User::find(2))
           ->visit('/home')
           ->waitForText('Message')
           ->type('message', 'Hey Taylor')
           ->press('Send');

    $first->waitForText('Hey Taylor')
          ->assertSee('Jeffrey Way');
});

正如你所看到的,如果你在 browse() 闭包中请求更多的参数,每个都将被传递一个新的浏览器会话,这样便可以与之交互。

第一个浏览器以用户 1 登录,访问主路由,然后等待(最多 5 秒),直到它看到 text “Message”,在此测试中,该消息表示页面上显示的聊天框。 接下来,以用户 2 登录,访问主路由,等待查看聊天框,然后在其中键入消息并点击发送。 最后,用户 1 监视该消息,并断言用户 2 的名字(我们假定名为 “Jeffrey Way” )显示出来。

loginAs 是以前被命名为 be()actingAs() 的方法,可以用来取 User 实例或用户 ID。

新断言#

Dusk 中的大多数断言与以前相同,但是很多都有了新的名称。 这里有整个 列表 的内容供你查看,而这下面列出了一些新的断言:

$browser->assertTitle('My App - Home');
$browser->assertTitleContains('My New Blog Post');
$browser->assertVisible('.chat-box');
$browser->assertMissing('.loading');

Dusk 页#

读取更长,更复杂的 Dusk 交互集合可能很难遵循,所以有一个可选的概念称为页面,使您很容易在 Dusk 测试中分组功能。 页面表示可用于导航到其的 URL,一组可以运行以确保浏览器仍在此页面上的断言,以及一组用于公共选择器的昵称。

创建页面#

要创建页面,请使用 Artisan 命令 dusk:page

php artisan dusk:page Dashboard

下面是为我们生成的 Dashboard 文件:

<?php

namespace Tests\Browser\Pages;

use Laravel\Dusk\Browser;
use Laravel\Dusk\Page as BasePage;

class Dashboard extends BasePage
{
    /**
     * Get the URL for the page.
     *
     * @return string
     */
    public function url()
    {
        return '/';
    }

    /**
     * Assert that the browser is on the page.
     *
     * @return void
     */
    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url());
    }

    /**
     * Get the element shortcuts for the page.
     *
     * @return array
     */
    public function elements()
    {
        return [
            '@element' => '#selector',
        ];
    }
}

很显然,在这上面, url() 方法指明该如何导航到此页面。而 assert() 方法则写明了:只要这个断言通过,我仍然在这个页面上。

elements() 数组创建了一个简写的选择器,使得你无论什么何时都可以使用它来引用元素。 综上所述,我们也可以这样来填写这些方法:

class Dashboard extends BasePage
{
    public function url()
    {
        return '/dashboard';
    }

    public function assert(Browser $browser)
    {
        $browser->assertPathIs($this->url());
    }

    public function elements()
    {
        return [
            '@createPost' => '#create-new-post-button',
            '@graphs' => '.dashboard__graphs',
        ];
    }
}

手动为每个网页上创建自定义的交互方法。 例如,测试中的一个常见行为可能是设置几个下拉列表,然后单击 “过滤” 按钮。 像这样:

// Dashboard
public function filterGraph($browser, $filterStatus)
{
    $browser->select('filterBy', $filterStatus)
        ->select('limit', 'one-month')
        ->press('Filter');
}

使用页面#

有几种不同的方式来使用一个页面。 首先,先访问它,它既指导浏览器,也加载我们的速记选择器:

use Tests\Browser\Pages\Dashboard;
...

$browser->visit(new Dashboard)
    ->assertSee('@graphs');

如果想通过其他地方的按钮访问 new Dashboard 页面 ,则可以使用 on() 方法来加载:

use Tests\Browser\Pages\Dashboard;
...

$browser->visit('/)
    ->type('email', 'matt@matt.com')
    ->type('password', 'secret')
    ->press('Log in')
    ->on(new Dashboard)
    ->assertSee('@graphs');

也可以使用自定义方法:

$browser->visit(new Dashboard)
    ->filterBy('donors')
    ->assertSee('Sally');

全局速记选择器#

创建全局速记选择器,在默认 test/Browser/Pages/Page 页面中在您的网站的任何位置使用,这是加载到每个页面。 将它们添加到 siteElements() 方法中即可。

// tests/Browser/Pages/Page
public static function siteElements()
{
    return [
        '@openChat' => '#chat-box__open-button',
    ];
}

杂记#

你已经看到了 Dusk 的强大之处。 下面是几个隐藏技能:

首先,可以在 .env.dusk.local(或 .env.dusk.任何你想要的测试的环境 )中创建自定义 Dusk 环境文件。

第二,有一些方法需要 jQuery 来选择页面上的内容。Dusk 会检查你的页面是否加载 jQuery,如果没有,会在测试期间注入它。

最后,任何时候测试失败,Dusk 会为你的报错页面截图存到 tests\Browser\Screenshots 目录。 这样一来你就可以看到页面的确切样子,相当便利的说~

file

小结#

好了,以上就是全部了~虽然,只要拉入旧的测试包,就仍然可以继续写之前一直写的测试。 但是现在有一个全新的世界对你开放。 我建议你不妨尝试一下。哈~

以上内容翻译改编自 Matt Stauffer 的 Laravel 5.4 新功能系列文章之 Introducing Laravel Dusk (new in Laravel 5.4)

本作品采用《CC 协议》,转载必须注明作者和本文链接
Stay Hungry, Stay Foolish.
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。