Acceptance Testing Laravel & VueJs Apps with Codeception

This tutorial is based on a fantastic tutorial by @themsaid.

Laravel's testing suite is great for testing php generated views, but unfortunately it doesn't render javascript and therefore everything written in VueJs is outside the scope of Laravel's built in tests.

My guess is we will see some answers to this problem in future versions of Laravel, but for now I've found that the best way to acceptance test Laravel/Vue applications is: Codeception (using selenium and a chrome driver).

This tutorial covers:

  • acceptance testing javascript-heavy apps with Codeception and Selenium
  • setting up and tearing down a testing database with each test
  • accessing Laravel factories, facades, and other helpers in your tests
  • automatically authenticating users in your tests

Check out this sample laravel application for more guidance: https://github.com/calebporzio/laravel-acceptance-example

Installing Codeception

The basics of getting up and running with Codeception are pretty simple. In your laravel app run the following commands:

$ composer require "codeception/codeception:*"
$ php vendor/bin/codecept bootstrap

You should notice lots of new files and folders in your "tests" directory and a new codeception.yml file in your root directory.

Because we are only interested in using the "Acceptance" testing features of Codeception, we can reorganize (and delete) our folder structure to look like this:

tests  
    - acceptance
        - _data
        - _output
        - _support
        - _envs
        - _bootstrap.php
    - _bootstrap.php
    - acceptance.suite.yml
    - ExampleTest.php
    - TestCase.php
codeception.yml  

and modify codeception.yml to look like this:

actor: Tester  
paths:  
    tests: tests
    log: tests/acceptance/_output
    data: tests/acceptance/_data
    support: tests/acceptance/_support
    envs: tests/acceptance/_envs
settings:  
...

Also, tell codeception your site url in tests/acceptance.suite.yml

class_name: AcceptanceTester  
modules:  
    enabled:
        - PhpBrowser:
            url: http://your-local-project.dev
        - \Helper\Acceptance

Now you should be able to run the following and not receive errors.

$ php vendor/bin/codecept run

Setting Up Selenium

At this point, your codeception tests will be run using "PhpBrowser" - a guzzle based, non-javascript browser, similar to Laravel / PhpUnit's Symfony DomCrawler. To be able to render javascript we need to set up Selenium.

Selenium has a standalone server that needs to be running for your tests to work. The server can be manipulated in many ways, for our purposes we want to use the Chrome WebDriver for our tests. Download the following files:

Create a tests/acceptance/bin directory and place both files there. Then navigate to that directory and execute the following command in a separate terminal window. (rename your files or modify the command for you filenames)

$ java -Dwebdriver.chrome.driver=./chromedriver -jar ./selenium-server-standalone.jar

Assuming you didn't get any errors (do you have java installed?) You now should have Selenium and Chrome WebDriver installed and running on your system.

Now, the last step is to tell Codeception to use Selenium in the tests/acceptance.suite.yml file:

class_name: AcceptanceTester  
modules:  
    enabled:
        - WebDriver:
            url: http://your-local-project.dev
            browser: chrome
        - \Helper\Acceptance

Phew... your all set up! Let's keep moving...

Your First Test

Ok, before we get more advanced, lets get a test up and running. To do this, create a new file: tests/acceptance/ExampleCept.php

<?php

$I = new AcceptanceTester($scenario);
$I->am('user'); // actor's role
$I->wantTo('view homepage'); // feature to test
$I->amOnPage('/');
$I->see('something on the page'); // replace this with something on your homepage
$I->amOnPage('/');

Now run the tests

$ php vendor/bin/codecept run

Congratulations, you wrote an acceptance test that renders javascript. Reference the available actions here.

What we have so far is great, if this is all you need then stop here. If you still want to:

  • use model factories to seed the database
  • set up and authenticate a user from my tests so I don't have to go through the registration / login process for every test
  • roll back any changes to the database from the test

... then keep following along ...

Using a Test Database

I don't want my tests to constantly be changing my local application's database. Therefore, it would be helpful to have a totally isolated database to test from.

Lets create a new testing sqlite database file: database/testing_db.sqlite

Now we need a way to change the database your Laravel app uses at runtime. We are going to use cookies to detect Selenium in your middleware.

We need to migrate the sqlite db and set a cookie from your test file: tests/acceptance/ExampleCept.php
Note: "setCookie" must come after at least one "amOnPage"

...
Artisan::call('migrate');  
...
$I->amOnPage('/');
$I->setCookie('selenium_request', 'true');
...

Setup a testing database config in config/database.php

...
'testing' => [  
    'driver' => 'sqlite',
    'database' => database_path('testing_db.sqlite'),
    'prefix' => '',
],
...

Create a middleware file to detect the cookie

$ php artisan make:middleware DetectSelenium

Add this to it

public function handle($request, Closure $next)  
{
    if (isset($_COOKIE['selenium_request']) && app()->isLocal()) {
        config(['database.default' => 'testing']);
    }

    return $next($request);
}

Now register it in app\Http\Kernel.php

protected $middleware = [  
    \App\Http\Middleware\DetectSelenium::class,
];

Great, now your tests are using a separate database and will automatically be "rolled-back" (the database gets re-dumped every test).

Using Factories

The goal here is to be able to write something like this:

...
$post = factory(\App\Post::class)->create();
$I->amOnPage('/posts');
$I->see($post->title);
...

To do this we will need to bootstrap our Laravel app within our tests so we can access fun things like models and factories. We will leverage tests/_bootsrap.php to achieve this. Feel free to reference Laravel's public/index.php file for some context.

<?php  
// This is global bootstrap for autoloading

require 'bootstrap/autoload.php';  
$app = require 'bootstrap/app.php';
$app->instance('request', new \Illuminate\Http\Request);
$app->make('Illuminate\Contracts\Http\Kernel')->bootstrap();
config(['database.default' => 'testing']);  

Now you should be able to use factories, facades, helper functions, and any other Laravel mechanisms you're used to taking advantage of in your tests.

Authenticating Users

This part is pretty simple. Let's keep with the cookie method of communication to tell our app under test we want to log a specific user in.

To achieve this, change your middleware from above like so:

public function handle($request, Closure $next)  
{
    if (isset($_COOKIE['selenium_request']) && app()->isLocal()) {  
        config(['database.default' => 'testing']);

        if (isset($_COOKIE['selenium_auth'])) {
            Auth::loginUsingId((int) $_COOKIE['selenium_auth']);
        }
    }

    return $next($request);
}

Now set the cookie from your test:

$user = factory(\App\User::class)->create();
$I->amOnPage('/');
$I->setCookie('selenium_request', 'true');
$I->setCookie('selenium_auth', (string) $user->id);
$I->amOnPage('/some/restricted/page');

You should now be able to test parts of your app that require authentication and seed your database with anything you need for the test.

Warning: be careful not to degrade the value of your acceptance tests by testing too many things in isolation. The idea here is to supply you with enough to get a realistic environment up and running and test as if a user was using your app.

Cleaning Things Up

You probably don't want to set specific cookies and run scripts in all your test files. I recommend cracking open tests/acceptance/_support/AcceptanceTester.php and making helper functions in there.

Here is what I've done for your reference:

public function setUpSession()  
{
    \Artisan::call('migrate');

    $this->amOnPage('/');
    $this->setCookie('selenium_request', 'true');
}

public function tearDownSession()  
{
    \Artisan::call('migrate:rollback');

    $this->resetCookie('selenium_auth');
    $this->resetCookie('selenium_request');
}

public function loginUserById($id)  
{
    $this->setCookie('selenium_auth', (string) $id);
}

Say Thank You