zenstruck/browser

为您的 Symfony 功能测试提供流畅的接口。

资助包维护!
kbond

安装次数: 927 766

依赖项: 16

建议者: 0

安全: 0

星标: 186

关注者: 5

分支: 17

开放问题: 25

v1.8.1 2024-02-21 15:32 UTC

README

CI Status Code Coverage

Symfony 的功能测试可能会很冗长。此库提供了一种表达性、可自动补全的流畅包装器,围绕 Symfony 的原生功能测试功能

public function testViewPostAndAddComment()
{
    // assumes a "Post" is in the database with an id of 3

    $this->browser()
        ->visit('/posts/3')
        ->assertSuccessful()
        ->assertSeeIn('title', 'My First Post')
        ->assertSeeIn('h1', 'My First Post')
        ->assertNotSeeElement('#comments')
        ->fillField('Comment', 'My First Comment')
        ->click('Submit')
        ->assertOn('/posts/3')
        ->assertSeeIn('#comments', 'My First Comment')
    ;
}

将此库与 zenstruck/foundry 结合使用,可以使您的测试更加简洁和具有表达性

public function testViewPostAndAddComment()
{
    $post = PostFactory::new()->create(['title' => 'My First Post']);

    $this->browser()
        ->visit("/posts/{$post->getId()}")
        ->assertSuccessful()
        ->assertSeeIn('title', 'My First Post')
        ->assertSeeIn('h1', 'My First Post')
        ->assertNotSeeElement('#comments')
        ->fillField('Comment', 'My First Comment')
        ->click('Submit')
        ->assertOn("/posts/{$post->getId()}")
        ->assertSeeIn('#comments', 'My First Comment')
    ;
}

安装

composer require zenstruck/browser --dev

可选,在您的 phpunit.xml 中启用提供的扩展

  • PHPUnit 8 或 9
<!-- phpunit.xml -->

<extensions>
    <extension class="Zenstruck\Browser\Test\BrowserExtension" />
</extensions>
  • PHPUnit 10+
<phpunit>
   ...
   <extensions>
      <bootstrap class="Zenstruck\Browser\Test\BrowserExtension" />
   </extensions>
</phpunit>

此扩展提供以下功能

  1. 拦截测试错误/失败,并将浏览器的源代码(以及适用时的截图/js控制台日志)保存到文件系统。
  2. 在您的测试套件完成后,在控制台中列出所有已保存的工件(源代码/截图/js控制台日志)的摘要。

用法

此库提供 2 种不同的 "浏览器":

  1. KernelBrowser:使用您的 Symfony Kernel 进行请求 (快速)
  2. PantherBrowser:使用 symfony/panther 向具有真实浏览器的 web 服务器发送请求,这允许测试 JavaScript (慢)

您可以通过使您的测试类使用 HasBrowser 特性在您的测试中使用这些浏览器

namespace App\Tests;

use PHPUnit\Framework\TestCase;
use Zenstruck\Browser\Test\HasBrowser;

class MyTest extends TestCase
{
    use HasBrowser;

    /**
     * Requires this test extends Symfony\Bundle\FrameworkBundle\Test\KernelTestCase
     * or Symfony\Bundle\FrameworkBundle\Test\WebTestCase.
     */
    public function test_using_kernel_browser(): void
    {
        $this->browser()
            ->visit('/my/page')
            ->assertSeeIn('h1', 'Page Title')
        ;
    }

    /**
     * Requires this test extends Symfony\Component\Panther\PantherTestCase.
     */
    public function test_using_panther_browser(): void
    {
        $this->pantherBrowser()
            ->visit('/my/page')
            ->assertSeeIn('h1', 'Page Title')
        ;
    }
}

所有浏览器都有以下方法

/** @var \Zenstruck\Browser $browser **/

$browser
    // ACTIONS
    ->visit('/my/page')
    ->click('A link')
    ->fillField('Name', 'Kevin')
    ->checkField('Accept Terms')
    ->uncheckField('Accept Terms')
    ->selectField('Canada') // "radio" select
    ->selectField('Type', 'Employee') // "select" single option
    ->selectField('Notification', ['Email', 'SMS']) // "select" multiple options
    ->selectField('Notification', []) // "un-select" all multiple options
    ->attachFile('Photo', '/path/to/photo.jpg')
    ->attachFile('Photo', ['/path/to/photo1.jpg', '/path/to/photo2.jpg']) // attach multiple files (if field supports this)
    ->click('Submit')

    // ASSERTIONS
    ->assertOn('/my/page') // by default checks "path", "query" and "fragment"
    ->assertOn('/a/page', ['path']) // check just the "path"

    // these look in the entire response body (useful for non-html pages)
    ->assertContains('some text')
    ->assertNotContains('some text')

    // these look in the html only
    ->assertSee('some text')
    ->assertNotSee('some text')
    ->assertSeeIn('h1', 'some text')
    ->assertNotSeeIn('h1', 'some text')
    ->assertSeeElement('h1')
    ->assertNotSeeElement('h1')
    ->assertElementCount('ul li', 2)
    ->assertElementAttributeContains('head meta[name=description]', 'content', 'my description')
    ->assertElementAttributeNotContains('head meta[name=description]', 'content', 'my description')

    // form field assertions
    ->assertFieldEquals('Username', 'kevin')
    ->assertFieldNotEquals('Username', 'john')

    // form checkbox assertions
    ->assertChecked('Accept Terms')
    ->assertNotChecked('Accept Terms')

    // form select assertions
    ->assertSelected('Type', 'Employee')
    ->assertNotSelected('Type', 'Admin')

    // form multi-select assertions
    ->assertSelected('Roles', 'Content Editor')
    ->assertSelected('Roles', 'Human Resources')
    ->assertNotSelected('Roles', 'Owner')

    // CONVENIENCE METHODS
    ->use(function() {
        // do something without breaking
    })

    ->use(function(\Zenstruck\Browser $browser) {
        // access the current Browser instance
    })

    ->use(function(\Symfony\Component\BrowserKit\AbstractBrowser $browser)) {
        // access the "inner" browser
    })

    ->use(function(\Symfony\Component\BrowserKit\CookieJar $cookieJar)) {
        // access the cookie jar
        $cookieJar->expire('MOCKSESSID');
    })

    ->use(function(\Zenstruck\Browser $browser, \Symfony\Component\DomCrawler\Crawler $crawler) {
        // access the current Browser instance and the current crawler
    })

    ->crawler() // Symfony\Component\DomCrawler\Crawler instance for the current response

    ->content() // string - raw response body

    // save the raw source of the current page
    // by default, saves to "<project-root>/var/browser/source"
    // configure with "BROWSER_SOURCE_DIR" env variable
    ->saveSource('source.txt')

    // the following use symfony/var-dumper's dump() function and continue
    ->dump() // raw response body
    ->dump('h1') // html element
    ->dump('foo') // if json response, array key
    ->dump('foo.*.baz') // if json response, JMESPath notation can be used

    // the following use symfony/var-dumper's dd() function ("dump & die")
    ->dd() // raw response body or array if json
    ->dd('h1') // html element
    ->dd('foo') // if json response, array key
    ->dd('foo.*.baz') // if json response, JMESPath notation can be used
;

KernelBrowser

此浏览器有以下方法

/** @var \Zenstruck\Browser\KernelBrowser $browser **/

$browser
    // response assertions
    ->assertStatus(200)
    ->assertSuccessful() // 2xx status code
    ->assertRedirected() // 3xx status code
    ->assertHeaderEquals('Content-Type', 'text/html; charset=UTF-8')
    ->assertHeaderContains('Content-Type', 'html')
    ->assertHeaderEquals('X-Not-Present-Header', null)

    // helpers for quickly checking the content type
    ->assertJson()
    ->assertXml()
    ->assertHtml()
    ->assertContentType('zip')

    // by default, exceptions are caught and converted to a response
    // use the BROWSER_CATCH_EXCEPTIONS environment variable to change default
    // this disables that behaviour allowing you to use TestCase::expectException()
    ->throwExceptions()

    // enable catching exceptions
    ->catchExceptions()

    // by default, the kernel is rebooted between requests
    // this disables this behaviour
    ->disableReboot()

    // re-enable rebooting between requests if previously disabled
    ->enableReboot()

    // enable the profiler for the next request (if not globally enabled)
    ->withProfiling()

    // by default, redirects are followed, this disables that behaviour
    // use the BROWSER_FOLLOW_REDIRECTS environment variable to change default
    ->interceptRedirects()

    // enable following redirects
    // if currently on a redirect response, follows
    ->followRedirects()

    // Follows a redirect if ->interceptRedirects() has been turned on
    ->followRedirect() // follows all redirects by default
    ->followRedirect(1) // just follow 1 redirect

    // combination of assertRedirected(), followRedirect(), assertOn()
    ->assertRedirectedTo('/some/page') // follows all redirects by default
    ->assertRedirectedTo('/some/page', 1) // just follow 1 redirect

    // combination of interceptRedirects(), withProfiling(), click()
    // useful for submitting forms and making assertions on the "redirect response"
    ->clickAndIntercept('button')

    // exception assertions for the "next request"
    ->expectException(MyException::class, 'the message')
    ->post('/url/that/throws/exception') // fails if above exception not thrown

    ->expectException(MyException::class, 'the message')
    ->click('link or button') // fails if above exception not thrown
;

// Access the Symfony Profiler for the last request
$queryCount = $browser
    // If profiling is not globally enabled for tests, ->withProfiling()
    // must be called before the request.
    ->profile()->getCollector('db')->getQueryCount()
;

// "use" a specific data collector
$browser->use(function(\Symfony\Component\HttpKernel\DataCollector\RequestDataCollector $collector) {
    // ...
})

身份验证

KernelBrowser 有身份验证辅助程序和断言

/** @var \Zenstruck\Browser\KernelBrowser $browser **/

$browser
    // authenticate a user for subsequent actions
    ->actingAs($user) // \Symfony\Component\Security\Core\User\UserInterface

    // If using zenstruck/foundry, you can pass a factory/proxy
    ->actingAs(UserFactory::new())

    // fail if authenticated
    ->assertNotAuthenticated()

    // fail if NOT authenticated
    ->assertAuthenticated()

    // fails if NOT authenticated as "kbond"
    ->assertAuthenticated('kbond')

    // \Symfony\Component\Security\Core\User\UserInterface or, if using
    // zenstruck/foundry, you can pass a factory/proxy
    ->assertAuthenticated($user)
;
身份验证故障排除

在调用 ->assertAuthenticated() 时抛出 LogicException: Cannot create the remember-me cookie; no master request available. 异常

这是当 token 是您的防火墙中 RememberMeTokenlazy: true 时发生的,并且之前的请求没有执行任何安全相关操作。可能的解决方案

  1. 在调用 ->assertAuthenticated() 之前,访问您知道会启动安全性的页面(例如,在 Twig 模板中的 is_granted())。
  2. 在发出上一个请求之前调用 ->withProfiling()。这启用了安全数据收集器,该收集器执行安全操作。
  3. 在您的测试环境中设置 framework.profiler.collect: true。这为所有请求启用分析器,从而消除了调用 ->withProfiling() 的需要,但可能会减慢您的测试速度。

HTTP 请求

KernelBrowser 可用于测试 API 端点。以下 http 方法可用

use Zenstruck\Browser\HttpOptions;

/** @var \Zenstruck\Browser\KernelBrowser $browser **/

$browser
    // http methods
    ->get('/api/endpoint')
    ->put('/api/endpoint')
    ->post('/api/endpoint')
    ->delete('/api/endpoint')

    // second parameter can be an array of request options
    ->post('/api/endpoint', [
        // request headers
        'headers' => ['X-Token' => 'my-token'],

        // request body
        'body' => 'request body',
    ])
    ->post('/api/endpoint', [
        // json_encode request body and set Content-Type/Accept headers to application/json
        'json' => ['request' => 'body'],

        // simulates an AJAX request (sets the X-Requested-With to XMLHttpRequest)
        'ajax' => true,
    ])

    // optionally use the provided Zenstruck\Browser\HttpOptions object
    ->post('/api/endpoint',
        HttpOptions::create()->withHeader('X-Token', 'my-token')->withBody('request body')
    )

    // sets the Content-Type/Accept headers to application/json
    ->post('/api/endpoint', HttpOptions::json())

    // json encodes value and sets as body
    ->post('/api/endpoint', HttpOptions::json(['request' => 'body']))

    // simulates an AJAX request (sets the X-Requested-With to XMLHttpRequest)
    ->post('/api/endpoint', HttpOptions::ajax())

    // simulates a JSON AJAX request
    ->post('/api/endpoint', HttpOptions::jsonAjax())
;

Json 断言

使用 JMESPath 表达式 对 json 响本进行断言。有关更多信息,请参阅 JMESPath 教程

注意 需要 mtdowling/jmespath.phpcomposer require --dev mtdowling/jmespath.php

/** @var \Zenstruck\Browser\KernelBrowser $browser **/
$browser
    ->get('/api/endpoint')
    ->assertJson() // ensures the content-type is application/json
    ->assertJsonMatches('foo.bar.baz', 1) // automatically calls ->assertJson()
    ->assertJsonMatches('foo.*.baz', [1, 2, 3])
    ->assertJsonMatches('length(foo)', 3)
    ->assertJsonMatches('"@some:thing"', 6) // note: special characters like : and @ need to be wrapped in quotes
;

// access the json "crawler"
$json = $browser
    ->get('/api/endpoint')
    ->json()
;

$json->assertMatches('foo.bar.baz', 1);
$json->assertHas('foo.bar.baz');
$json->assertMissing('foo.bar.boo');
$json->search('foo.bar.baz'); // mixed (the found value at "JMESPath expression")
$json->decoded(); // the decoded json
(string) $json; // the json string pretty-printed

// "use" the json crawler
$json = $browser
    ->get('/api/endpoint')
    ->use(function(\Zenstruck\Browser\Json $json) {
        // Json acts like a proxy of zenstruck/assert Expectation class
        $json->hasCount(5);
        $json->contains('foo');
        // assert on children: the closure gets Json object contextualized on given selector
        // {"foo": "bar"}
        $json->assertThat('foo', fn(Json $json) => $json->equals('bar'))
        // assert on each element of an array
        // {"foo": [1, 2, 3]}
        $json->assertThatEach('foo', fn(Json $json) => $json->isGreaterThan(0));
        // assert json matches given json schema
        $json->assertMatchesSchema(file_get_contents('/path/to/json-schema.json'));
    })
;

注意 请参阅 完整的 zenstruck/assert 预期 API 文档,以查看 Zenstruck\Browser\Json 上所有可用的方法。

PantherBrowser

在 1.0 中,PantherBrowser 是实验性的,并且可能会受到 BC 断裂的影响。

此浏览器有以下额外的方法

/** @var \Zenstruck\Browser\PantherBrowser $browser **/

$browser
    // pauses the tests and enters "interactive mode" which
    // allows you to investigate the current state in the browser
    // (requires the env variable PANTHER_NO_HEADLESS=1)
    ->pause()

    // take a screenshot of the current browser state
    // by default, saves to "<project-root>/var/browser/screenshots"
    // configure with "BROWSER_SCREENSHOT_DIR" env variable
    ->takeScreenshot('screenshot.png')

    // save the browser's javascript console error log
    // by default, saves to "<project-root>/var/browser/console-log"
    // configure with "BROWSER_CONSOLE_LOG_DIR" env variable
    ->saveConsoleLog('console.log')

    // check if element is visible in the browser
    ->assertVisible('.selector')
    ->assertNotVisible('.selector')

    // wait x milliseconds
    ->wait(1000) // 1 second

    ->waitUntilVisible('.selector')
    ->waitUntilNotVisible('.selector')
    ->waitUntilSeeIn('.selector', 'some text')
    ->waitUntilNotSeeIn('.selector', 'some text')

    ->doubleClick('Link')
    ->rightClick('Link')

    // dump() the browser's console error log
    ->dumpConsoleLog()

    // dd() the browser's console error log
    ->ddConsoleLog()

    // dd() and take screenshot (default filename is "screenshot.png")
    ->ddScreenshot()
;

多个浏览器实例

在您的测试中,您可以多次调用->xBrowser()方法以获取不同的浏览器实例。这对于测试具有实时功能的应用(例如websockets)非常有用。

namespace App\Tests;

use Symfony\Component\Panther\PantherTestCase;
use Zenstruck\Browser\Test\HasBrowser;

class MyTest extends PantherTestCase
{
    use HasBrowser;

    public function testDemo(): void
    {
        $browser1 = $this->pantherBrowser()
            ->visit('/my/page')
            // ...
        ;

        $browser2 = $this->pantherBrowser()
            ->visit('/my/page')
            // ...
        ;
    }
}

配置

有多个环境变量可供配置。

扩展

测试浏览器配置

您可以通过重写HasBrowser特性中的xBrowser()方法,在测试中配置浏览器默认选项或起始状态。

namespace App\Tests;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Browser\KernelBrowser;
use Zenstruck\Browser\Test\HasBrowser;

class MyTest extends KernelTestCase
{
    use HasBrowser {
        browser as baseKernelBrowser;
    }

    public function testDemo(): void
    {
        $this->browser()
            ->assertOn('/') // browser always starts on the homepage (as defined below)
        ;
    }

    protected function browser(): KernelBrowser
    {
        return $this->baseKernelBrowser()
            ->interceptRedirects() // always intercept redirects
            ->throwExceptions() // always throw exceptions
            ->visit('/') // always start on the homepage
        ;
    }
}

组件

组件是封装常见任务的组件对象。这些扩展Zenstruck\Browser\Component,并可以注入到浏览器的->use()可调用中。

/** @var \Zenstruck\Browser $browser **/

$browser
    ->use(function(MyComponent $component) {
        $component->method();
    })
;

邮件组件

请参阅https://github.com/zenstruck/mailer-test#zenstruckbrowser-integration

自定义组件

您可能有一些页面或页面部分,在测试中经常使用特定的操作/断言。您可以将这些封装成组件。以下是一个创建CommentComponent的例子,以展示这一功能。

namespace App\Tests;

use Zenstruck\Browser\Component;
use Zenstruck\Browser\KernelBrowser;

/**
 * If only using this component with a specific browser, this type hint can help your IDE.
 *
 * @method KernelBrowser browser()
 */
class CommentComponent extends Component
{
    public function assertHasNoComments(): self
    {
        $this->browser()->assertElementCount('#comments li', 0);

        return $this; // optionally make methods fluent
    }

    public function assertHasComment(string $body, string $author): self
    {
        $this->browser()
            ->assertSeeIn('#comments li span.body', $body)
            ->assertSeeIn('#comments li span.author', $author)
        ;

        return $this;
    }

    public function addComment(string $body, string $author): self
    {
        $this->browser()
            ->fillField('Name', $author)
            ->fillField('Comment', $body)
            ->click('Add Comment')
        ;

        return $this;
    }

    protected function preAssertions(): void
    {
        // this is called as soon as the component is loaded
        $this->browser()->assertSeeElement('#comments');
    }

    protected function preActions(): void
    {
        // this is called when the component is loaded but before
        // preAssertions(). Useful for page components where you
        // need to navigate to the page:
        // $this->browser()->visit('/contact');
    }
}

在测试中访问和使用这个新组件。

/** @var \Zenstruck\Browser $browser **/

$browser
    ->visit('/post/1')
    ->use(function(CommentComponent $component) {
        // the function typehint triggers the component to be loaded,
        // preActions() run and preAssertions() run

        $component
            ->assertHasNoComments()
            ->addComment('comment body', 'Kevin')
            ->assertHasComment('comment body')
        ;
    })
;

// you can optionally inject multiple components into the ->use() callback
$browser->use(function(Component1 $component1, Component2 $component2) {
    $component1->doSomething();
    $component2->doSomethingElse();
});

自定义HttpOptions

如果您发现自己创建了大量的http请求具有相同的选项(例如X-Token头),有几种方法可以减少这种重复。

  1. 使用->setDefaultHttpOptions()为当前浏览器设置默认选项。

    /** @var \Zenstruck\Browser\KernelBrowser $browser **/
    
    $browser
        ->setDefaultHttpOptions(['headers' => ['X-Token' => 'my-token']])
    
        // now all http requests will have the X-Token header
        ->get('/endpoint')
    
        // "per-request" options will be merged with the default
        ->get('/endpoint', ['headers' => ['Another' => 'Header']])
    ;
  2. 在测试用例的默认浏览器配置中使用->setDefaultHttpOptions()

    namespace App\Tests;
    
    use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
    use Zenstruck\Browser\KernelBrowser;
    use Zenstruck\Browser\Test\HasBrowser;
    
    class MyTest extends KernelTestCase
    {
        use HasBrowser {
            browser as baseKernelBrowser;
        }
    
        public function testDemo(): void
        {
            $this->browser()
                // all http requests in this test class will have the X-Token header
                ->get('/endpoint')
    
                // "per-request" options will be merged with the default
                ->get('/endpoint', ['headers' => ['Another' => 'Header']])
            ;
        }
    
        protected function browser(): KernelBrowser
        {
            return $this->baseKernelBrowser()
                ->setDefaultHttpOptions(['headers' => ['X-Token' => 'my-token']])
            ;
        }
    }
  3. 创建一个自定义的HttpOptions对象。

    namespace App\Tests;
    
    use Zenstruck\Browser\HttpOptions;
    
    class AppHttpOptions extends HttpOptions
    {
        public static function api(string $token, $json = null): self
        {
            return self::json($json)
                ->withHeader('X-Token', $token)
            ;
        }
    }

    然后,在您的测试中

    use Zenstruck\Browser\HttpOptions;
    
    /** @var \Zenstruck\Browser\KernelBrowser $browser **/
    
    $browser
        // instead of
        ->post('/api/endpoint', HttpOptions::json()->withHeader('X-Token', 'my-token'))
    
        // use your ApiHttpOptions object
        ->post('/api/endpoint', AppHttpOptions::api('my-token'))
    ;
  4. 创建一个带有您自己的请求方法(例如->apiRequest())的自定义浏览器

自定义浏览器

您可能希望添加自己的操作和断言。您可以通过创建自己的扩展Browser来实现这一点,该浏览器扩展了某个实现。然后,您可以通过使用基础浏览器方法添加自己的操作/断言。

namespace App\Tests;

use Zenstruck\Browser\KernelBrowser;

class AppBrowser extends KernelBrowser
{
    public function assertHasToolbar(): self
    {
        return $this->assertSeeElement('#toolbar');
    }
}

然后,根据您扩展的实现,设置适当的env变量

  • KernelBrowser: KERNEL_BROWSER_CLASS
  • PantherBrowser: PANTHER_BROWSER_CLASS

对于上面的例子,您需要设置KERNEL_BROWSER_CLASS=App\Tests\AppBrowser

提示:创建一个基础功能测试用例,以便所有测试都可以使用您的自定义浏览器,并使用@method注解来确保测试可以自动完成您的自定义方法。

namespace App\Tests;

use App\Tests\AppBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Zenstruck\Browser\Test\HasBrowser;

/**
 * @method AppBrowser browser()
 */
abstract class MyTest extends WebTestCase
{
    use HasBrowser;
}

扩展

这些是可以添加到自定义浏览器的特型。

邮件扩展

请参阅https://github.com/zenstruck/mailer-test#zenstruckbrowser-integration

自定义扩展

您可以为重复性任务创建自己的扩展。以下是一个创建用于登录/登出用户并对用户认证状态进行断言的AuthenticationExtension的例子。

namespace App\Tests\Browser;

trait AuthenticationExtension
{
    public function loginAs(string $username, string $password): self
    {
        return $this
            ->visit('/login')
            ->fillField('email', $username)
            ->fillField('password', $password)
            ->click('Login')
        ;
    }

    public function logout(): self
    {
        return $this->visit('/logout');
    }

    public function assertLoggedIn(): self
    {
        $this->assertSee('Logout');

        return $this;
    }

    public function assertLoggedInAs(string $user): self
    {
        $this->assertSee($user);

        return $this;
    }

    public function assertNotLoggedIn(): self
    {
        $this->assertSee('Login');

        return $this;
    }
}

添加到您的自定义浏览器中。

namespace App\Tests;

use App\Tests\Browser\AuthenticationExtension;
use Zenstruck\Browser\KernelBrowser;

class AppBrowser extends KernelBrowser
{
    use AuthenticationExtension;
}

在测试中使用

public function testDemo(): void
{
    $this->browser()
        // goes to the /login page, fills email/password fields,
        // and presses the Login button
        ->loginAs('[email protected]', 'password')

        // asserts text "Logout" exists (assumes you have a logout link when users are logged in)
        ->assertLoggedIn()

        // asserts email exists as text (assumes you display the user's email when they are logged in)
        ->assertLoggedInAs('[email protected]')

        // goes to the /logout page
        ->logout()

        // asserts text "Login" exists (assumes you have a login link when users not logged in)
        ->assertNotLoggedIn()
    ;
}