Testing

Table des matières

1. PHPUnit

https://laravel.com/docs/5.6/http-tests

Laravel is using PHPUnit and is already available: just open a prompt and start vendor\bin\phpunit.bat.

The PHPUnit package is installed as a dependency of Laravel in development mode.

1.1. Start PHPUnit

While vendor\bin\phpunit.bat is accessible, the .php script can be manually fired like this:

php vendor\phpunit\phpunit\phpunit -h

1.2. Create a new test

To create a test controller, just run:

php artisan make:test MyControllerTest

Attention: the filename SHOULD ends with Test.php like MyControllerTest.php. PHP_Unit will only process such files while getting the list of .php to process.

This will create the file /tests/Feature/MyControllerTest.php with this content:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class MyControllerTest extends TestCase
{
  public function testExample()
  {
    $this->assertTrue(true);
  }
}

To run it, we just need to open a DOS session and call PHPUnit:

vendor\bin\phpunit.bat

Note: we can also add options like, f.i.:

vendor\bin\phpunit.bat --stop-on-error

The list of options can be found at https://phpunit.de/manual/6.5/en/textui.html. Also by running:

vendor\bin\phpunit.bat -h

The result should be:

PHPUnit 7.2.7 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 171 ms, Memory: 12.00MB

OK (1 test, 1 assertion)

1.3. Try assertTrue if false

By updating the function to false, we’ll then fake an error:

<?php

namespace Tests\Feature;

use Tests\TestCase;

class MyControllerTest extends TestCase
{
  public function testExample()
  {
    $this->assertTrue(false);      ' â&#134;&#144; false will then invalidate the assertion
  }
}

Now, we’ve one error. PHPUnit is also reporting information’s about the file (TodoControllerTest.php), the line (16), the name of the function (testExample) and the encountered error (`Failed asserting that false is true.) so, we’ve all needed information’s for retrieving the failure.

PHPUnit 7.2.7 by Sebastian Bergmann and contributors.

F                                                                   1 / 1 (100%)

Time: 169 ms, Memory: 12.00MB

There was 1 failure:

1) Tests\Feature\TodoControllerTest::testExample
Failed asserting that false is true.

C:\Christophe\Repository\laravel_todos\tests\Feature\TodoControllerTest.php:16

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

1.4. Add some test

Update the testExample function in file /tests/Feature/MyControllerTest.php like this:

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

  $response = $this->call('GET', 'login');
  $response->assertStatus(200);

  // If not logged-in, the creation form should redirect (302)
  // to the login screen
  $response = $this->call('GET', route('todos.create'));
  $response->assertStatus(302);
  $response->assertSee('<title>Redirecting to ' . route('login') . '</title>');
}

The code is pretty simple to read.

  1. We’ll make a request to the / URL and check that the HTTP return code is well 200. If not, PHPUnit will raise an error.
  2. We’ll make a request to the login URL and also check if we’ve a code 200.
  3. We’ll try to display the create form by calling the named route. Such URL is protected by the auth middleware so, we expect a code 302 (i.e. a redirection) and the content should have a <title> tag like <title>Redirecting to [...]/login</title>. The part in brackets is the redirected URL (generated by the login named route).

1.5. Posting data

Example of a Post test:

public function testContactForm()
{
  $postData = [
    'name' => 'Joe Example',
    'email' => 'email-address',
    'message' => 'I love your website'
  ];

  $this->call('POST', '/contact', $postData);

  // Do some assertions

}

1.6. More complex example

We’ll add additional features to the TestCase class: the ability to output informations to the client (the CLI):

<?php

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    public function setup()
    {
        parent::setup();

        // Get the name of the calling function (f.i. "testPostingData")
        $testName = '* Running unit test for: ' . $this->getName() . ' *';
        $box = str_repeat('*', strlen($testName)) . PHP_EOL;

        // Display the name in the CLI
        self::output(PHP_EOL . $box . $testName . PHP_EOL . $box . PHP_EOL);
    }

    /**
     * Display information to the CLI
     *
     * @param  string $line Sentence to display
     * @return bool
     */
    public function output(string $line) : bool
    {
        echo $line . PHP_EOL;

        return true;
    }
}

1.6.1. Patch TestCase.php

Our tests extends the /tests/TestCase.php:

<?php

namespace Tests\Feature;

use Tests\TestCase;
use Auth;
use app\Todo;
use Faker;

class TodoControllerTest extends TestCase
{
    private $email = 'christophe@todos.com';
    private $password = 'admin';

    /**
     * Test a few URLs as a guest
     *
     * @return boolean
     */
    public function testGuestURLs() : bool
    {
        // To be sure, start the tests as guest
        if (Auth::check()) {
            Auth::logout();
        }

        // These routes are publicly available
        // The expected HTTP code is 200 ("Ok")
        $arr = ['todos.index', 'login', 'password.request'];

        foreach ($arr as $name) {
            $url = route($name);
            $this->output('Check 200 for ' . $url);

            $response = $this->call('GET', $url);
            $response->assertStatus(200);
        }

        // These routes are restricted to logged-in users
        // so the expected HTTP code is 302 ("HTTP redirection") and
        // should redirect to the login screen
        $uri = route('login');

        $arr = [
            'todos.create' => 'GET',
            'todos.edit' => 'GET',
            'todos.destroy' => 'DELETE',
            'todos.update' => 'PUT'
    ];

        foreach ($arr as $name => $method) {
            // Except for create, we need to provide an ID
            if ($name == 'todos.create') {
                $url = route($name);
            } else {
        // Fake ID
                $url = route($name, '9999');
            }

            $this->output('Check 302 for [' . $method . '] ' . $url);

            $response = $this->call($method, $url);
            $response->assertRedirect($uri);
        }

        return true;
    }

    /**
     * Try posting some data and check
     *
     * @return boolean
     */
    public function testPosting() : bool
    {
        self::output('Connect as ' . $this->email . ' and ' .
            'post some data');

        // Connect
        $user = [
            'email' => $this->email,
            'password' => $this->password
        ];

        if (Auth::attempt($user)) {
            // Ok, we're logged-in

            $faker = Faker\Factory::create('fr_FR');

            $postData = [
                'title' => $faker->sentence($nbWords = 6, $variableNbWords = true),
                'description' => 'Filled in by PHPUnit'
            ];

            // Submit the record
            $response = $this->call('POST', route('todos.store'), $postData);

            // The response would be a redirection to the URL
            // for displaying the detail of the inserted record
            // 1. Retrieve the last inserted record; get his ID
            $id = Todo::max('id');

            // 2. Build the URI to that page
            // (f.i. http://127.0.0.1/todos/33
            $uri = route('todos.show', $id);

            self::output('Create new Todo #' . $id . ' and check returned URI');

            // And now assert that it's fine
            $response->assertRedirect($uri);
        }

        return true;

    }
}

The test scenario is composed of two functions, one for testing public URLs and the second needs to first make a login.

1.6.1.1. testGuestURLs

First make sure we aren’t logged-in:

if (Auth::check()) {
    Auth::logout();
}

Then check a few routes. These routes exists and should answer with a HTTP 200 code.

We’re using $this->outpu() i.e. the function we just implement in the TestCase class.

$arr = ['todos.index', 'login', 'password.request'];

foreach ($arr as $name) {
  $url = route($name);
  $this->output('Check 200 for ' . $url);

  $response = $this->call('GET', $url);
  $response->assertStatus(200);
}```

Then check a few route that are only accessible when the user is logged in so, requesting these URI should, always, return a HTTP 302 redirection.

// These routes are restricted to logged-in users
  // so the expected HTTP code is 302 ("HTTP redirection") and
  // should redirect to the login screen
  $uri = route('login');

  $arr = [
    'todos.create' => 'GET',
    'todos.edit' => 'GET',
    'todos.destroy' => 'DELETE',
    'todos.update' => 'PUT'
  ];

  foreach ($arr as $name => $method) {
    // Except for create, we need to provide an ID
    if ($name == 'todos.create') {
      $url = route($name);
    } else {
      // Fake ID
      $url = route($name, '9999');
    }

    $this->output('Check 302 for [' . $method . '] ' . $url);

    $response = $this->call($method, $url);
    $response->assertRedirect($uri);
  }
1.6.1.2. testPosting

Now make sure we are logged-in:

// Connect
$user = [
  'email' => $this->email,
  'password' => $this->password
];

if (Auth::attempt($user)) {
  // Ok, we're logged-in
}

Simulate the creation form and submit data:

use Faker;

[...]

$faker = Faker\Factory::create('fr_FR');

$postData = [
  'title' => $faker->sentence($nbWords = 6, $variableNbWords = true),
  'description' => 'Filled in by PHPUnit'
];

// Submit the record
$response = $this->call('POST', route('todos.store'), $postData);

Retrieve the last ID in the table since the POST should have created a new entry then compare the returned URI with the one to the last record; they should be equals.

// The response would be a redirection to the URL
// for displaying the detail of the inserted record
// 1. Retrieve the last inserted record; get his ID
$id = Todo::max('id');

// 2. Build the URI to that page
// (f.i. http://127.0.0.1/todos/33
$uri = route('todos.show', $id);

self::output('Create new Todo #' . $id . ' and check returned URI');

// And now assert that it's fine
$response->assertRedirect($uri);

2. Dusk

https://github.com/laravel/dusk

https://scotch.io/tutorials/introduction-to-laravel-dusk

Laravel Dusk was one of the new features introduced in Laravel 5.4. Dusk is a tool for application testing. Dusk will run tests in a browser while PHPUnit is making HTTP requests.

2.1. Installation

Tutorial on Laravel : https://laravel.com/docs/5.4/dusk

composer require --dev laravel/dusk

followed by

php artisan dusk:install

This will create the folder /tests/Browser in the folder’s application.