Backend Development

Laravel Dusk And Browser Testing Automation

Laravel Dusk And Browser Testing Automation

In this post we will learn about how to use the laravel dusk package to write unit tests for browser and GUI interactions and events.

 

 

 

Usually when we write unit tests for particular project, we focus only on writing the tests for backend logic only and the PHP code such as testing JSON services and testing data retrieval from databases. But it is also necessary to test the interactions that occur in the browser when executing a certain action.

For this purpose the laravel Dusk package can be a good fit for scenarios like these. Laravel Dusk provides a convenient Api for writing unit test for browser simulation actions.

 

What Laravel Dusk Can Do

Laravel Dusk provides browser testing and cover most the actions in the browser like:

  • Link and Button clicks
  • Browser navigation and redirection.
  • Form manipulation and submission.
  • Keyboard and mouse events like mousedown, mouseenter, mouseleave, etc.
  • Executing javascript code and retrieving the output.

 

For the purpose of this project we will create a laravel project:

composer create-project laravel/laravel lara_dusk

Next let’s install the Laravel Dusk package:

composer require --dev laravel/dusk

Laravel Dusk uses ChromeDriver behind the scenes to launch a stateless browser daemon process. I will be using the ChromeDriver in this tutorial. If you need to use another driver refer to dusk docs in order to learn more.

Now execute the dusk:install command:

php artisan dusk:install

This command creates tests/Browser directory where the dusk tests will be located. Also it downloads the ChromeDriver binaries, these binaries will be downloaded in vendor/laravel/dusk/bin.

 

As an important note you must set the APP_URL environment variable to match the url you use when accessing the application in the browser otherwise the Dusk will be unable to check the test routes and tests will fail.

Writing Tests

Let’s create a simple test using dusk:make command. This test simulates a landing page is loaded.

php artisan dusk:make LandingPageTest

This command generate a test file LandingPageTest.php in tests/Browser/ directory. Before checking the code inside the test file we need to create a view file and route.

Create view file resources/views/landing.blade.php:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Landing page</title>
    
    <style>
        body {
            font-family: 'Nunito', sans-serif;
        }
    </style>
</head>
<body class="antialiased" style="text-align: center">
    <h1 style="display: none; color: #3eb5de" id="welcome" dusk="welcometext">Welcome To Our Website</h1>

    <script>
        setTimeout(() => {
            document.getElementById("welcome").style.display = "block";
        }, 2000);
    </script>
</body>
</html>

This simple page have a welcome text that appears after some amount of time. Note the special attribute “dusk” we added above, this is not HTML attribute instead we will use this attribute with Dusk tests when selecting elements.

Open the routes/web.php and add a route for this page:

Route::view('/landing', 'landing');

Now go to http://localhost:8000/landing you should see the text appear after 2 seconds. The next step is to write a test that verifies that the page is loaded and the text appears on the page.

Open the previous created test file LandingPageTest.php you will see this method:

public function testExample()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('/')
                    ->assertSee('Laravel');
        });
    }

This is the default testExample() method that it’s automatically generated. In this code all the tests should be inside the browse() method which accepts a closure as a parameter.

Modify this method signature like so:

public function testShowWelcomeMessage()
    {
        $this->browse(function (Browser $browser) {
            $browser->visit('landing')
                    ->assertVisible('Welcome To Our Website');
        });
    }

Run the tests:

php artisan dusk

This command will run the tests in the all test files, but in our case let’s run the test in the previous file only by passing the test file:

php artisan dusk tests/Browser/LandingPageTest.php

After running this command a socket connection will be triggered and you will see the test results like so:

There was 1 failure:

1) Tests\Browser\LandingPageTest::testShowWelcomeMessage
Did not see expected text [Welcome To Our Website] within element [body].        
Failed asserting that false is true.

Now our test file is tested but the test is failed, and the reason for this is that the text appear in the page after some time. So we need to tell Dusk to wait some amount of time before asserting. This can be done easily using the waitFor(selector) method

Modify the browse() method to be:

$this->browse(function (Browser $browser) {
            $browser->visit('landing')
                ->waitFor('@welcometext')
                    ->assertVisible('@welcometext');
        });

By running this test we should see that the test succeeded:

DevTools listening on ws://127.0.0.1:65349/devtools/browser/afa9977f-13db-40f0-a409-cc7724cc6be7
.                                                                   

Time: 00:01.917, Memory: 20.00 MB

OK (1 test, 1 assertion)

The waitFor(selector) accepts a css selector or dusk selector and waits for a maximum amount of 5 seconds until the element appear in page, you can pass a second argument to increase modify amount. In our example i passes a dusk selector which prefixed by “@” character.

Most of the time you will be using waitFor() because webpage elements usually uses animations and perform ajax operations and take some to appear, for this there are many similar methods like waitFor():

  • waitForTextIn(selector, text) 
  • waitUntilMissing(selector)
  • waitUntilEnabled(selector, seconds = 5),
  • waitUntilDisabled(selector, seconds = 5)
  • waitForText(text, seconds = 5)
  • waitUntilMissingText(text, seconds = 5)
  • waitForLink(linkText, seconds = 5)
  • waitForInput(field, seconds = 5)

Refer to laravel docs to check all the waitFor() methods.

The previous test can rewritten using waitForText():

$this->browse(function (Browser $browser) {
            $browser->visit('landing')
                ->waitForText('Welcome To Our Website')
                ->assertSee('Welcome To Our Website');
        });

As you see this is a simple example to show the idea, let’s move to show how to apply this to forms.

 

Forms Testing

As we have done the testing to the simple page in the previous, the same concept applies when testing form submissions, but note that forms testing can be more tricky.

In this example we have a simple registration form, once the user enters their details and click submit and a message show on the screen and the data is saved into the database.

In order to run the migrations, create a new mysql database and modify the .env file like so:

.env

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lara_dusk
DB_USERNAME=<db username>
DB_PASSWORD=<db password>

Now migrate the tables:

php artisan migrate

After this command runs successfully you will see the created tables in the database.

Now i will make a sample code for handle user registration, you don’t have to understand this code as this is just for demonstration purposes.

Create a new layout file:

resources/views/layouts/layout.blade.php

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Laravel</title>

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">

    <style>
        body {
            font-family: 'Nunito', sans-serif;
        }

        input, button {
            padding: 10px;
        }

        button {
            background: #3eb5de;
            cursor: pointer;
        }

        span.error {
            color: red;
        }

        .alert-success {
            background: #86e986;
            font-size: 20px;
        }
    </style>
</head>
<body class="antialiased">
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0">
    @if (Route::has('login'))
        <div class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
            @auth
                <a href="{{ url('/home') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Home</a>
            @else
                <a href="{{ route('login') }}" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</a>

                @if (Route::has('register'))
                    <a href="{{ route('register') }}" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</a>
                @endif
            @endauth
        </div>
    @endif

       @yield('content')

</div>
</body>
</html>

Also create the register view:

resources/views/register.blade.php

@extends('layouts.layout')

@section('content')

    <div class="px-8 py-6 mx-4 mt-4 text-left shadow-lg">
        <h3 class="text-2xl font-bold text-center">Join us</h3>
        <form method="post" action="{{route('register')}}">
            @csrf
            <div class="mt-4">
                <div class="flex">
                    <label class="block" for="Name">Name<label>
                            <input type="text" name="name" placeholder="Name"
                                   class="w-full px-4 py-2 mt-2">
                </div>
                @error('name') <span class="error">{{$message}}</span> @enderror

                <div class="mt-4">
                    <label class="block" for="email">Email<label>
                            <input type="text" name="email" placeholder="Email"
                                   class="w-full px-4 py-2 mt-2">
                </div>
                @error('email') <span class="error">{{$message}}</span> @enderror

                <div class="mt-4">
                    <label class="block">Password<label>
                            <input type="password" name="password" placeholder="Password"
                                   class="w-full px-4 py-2 mt-2">
                </div>
                @error('password') <span class="error">{{$message}}</span> @enderror

                <div class="mt-4">
                    <label class="block">Confirm Password<label>
                            <input type="password" name="password_confirmation" placeholder="Password"
                                   class="w-full px-4 py-2 mt-2">
                </div>
                <div class="flex">
                    <button type="submit" dusk="submit" class="w-full px-6 py-2 mt-4 text-white bg-blue-600">Create
                        Account</button>
                </div>
            </div>
        </form>
    </div>

@endsection

Create another view for the home:

resources/views/home.blade.php

@extends('layouts.layout')

@section('content')

    @if(\Session::has('success'))
        <div class="p-4 mb-4 alert-success" role="alert">
            {{\Session::get('success')}}
        </div>
    @endif

@endsection

Create the RegisterController

app/Http/Controllers/RegisterController.php

<?php

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;

class RegisterController extends Controller
{
    public function register()
    {
        return view('register');
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'name' => 'required',
            'email' => 'required|email|unique:users,email',
            'password' => 'required|confirmed'
        ]);

        $user = new User();
        $user->name = $request->name;
        $user->email = $request->email;
        $user->password = bcrypt($request->password);

        $user->save();

        auth()->login($user);

        return redirect()->to('/')->with("success", "User registered successfully");
    }
}

Then update the routes to be as shown:

routes/web.php

Route::get('/', function () {
    return view('home');
});

Route::get('/register', [\App\Http\Controllers\RegisterController::class, 'register']);
Route::post('/store', [\App\Http\Controllers\RegisterController::class, 'store'])->name('register');

At this point we can create our test file, but before that we need to identify the steps that the dusk test file will do in order to assert the registration:

  • First we visit the /register route.
  • Once visited we have to fill all the form fields like name, email, etc.
  • After that press the submit button to submit the form.
  • If the form validation is successful and the user data is saved then we must verify that we redirect to home page “/”.

According to these steps let’s create the dusk test that simulates this form process:

php artisan dusk:make RegisterPageTest

Open the test file and modify it like so:

tests/Browser/RegisterPageTest.php

<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class RegisterPageTest extends DuskTestCase
{
    use DatabaseMigrations;

    /**
     * A Dusk test example.
     *
     * @return void
     */
    public function testUserRegister()
    {
        $user = [
            'name' => 'Wael Salah',
            'email' => 'wael.fci@gmail.com',
            'password' => '123456'
        ];

        $this->browse(function (Browser $browser) use ($user) {
            $browser->visit('register')
                ->type('name', $user['name'])
                ->type('email', $user['email'])
                ->type('password', $user['password'])
                ->type('password_confirmation', $user['password'])
                ->press('@submit')
                ->assertPathIs('/');

        });
    }
}

In this code i prepared some dummy data to fill the registration form. Then inside the browse() method callback i invoked Browser::visit(‘register’), to navigate to the “/register” page.

To fill a form field i called Browser::type(field, value) method passing in the field name or css selector or dusk selector and the value, so i called type() four times for each of the form fields.

Next i clicked the submit button using the press() method passing in the dusk selector “@submit”. Finally i called the assertPathIs() method which verifies the the user will be redirect to home page “/” after successful submission.

In the top of the class i make use of the DatabaseMigrations trait, this is necessary step here because we are dealing with database. What this trait does is to create the database migrations when the test start by calling the up() method on each migration and it drops the tables when the test ends by calling the down() method on each migration. So you will note that the database tables is dropped when you finish the test.

Now run the dusk test against this file:

php artisan dusk tests/Browser/RegisterPageTest.php

The test will take couple of seconds and after that it show success:

Time: 00:04.029, Memory: 26.00 MB

OK (1 test, 1 assertion)

If you we want the test to fail, you can make one of the $user array empty, i.e empty the password field:

$user = [
            'name' => 'Wael Salah',
            'email' => 'wael.fci@gmail.com',
            'password' => ''
        ];

Now when running the test again it will show:

Actual path [/register] does not equal expected path [/].
Failed asserting that '/register' matches PCRE pattern "/^\/$/u".

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

The test failed because the form validations triggered and the user can’t proceed to the homepage “/”.

 

From the example above we saw the method type() which enters data to input fields like text boxes or numeric fields. However Laravel Dusk provides many other methods for dealing with almost all form field types like:

  • typeSlowly(fieldname, value): same as type() but this method attempts to pause 100 ms between keypress.
  • select(fieldname, value): For dealing with dropdowns.
  • check(fieldname), uncheck(fieldname): For dealing with checkboxes.
  • radio(fieldname, value): Dealing with radio buttons.
  • attach(fieldname, filePath): For dealing with file uploads.
  • append(fieldname, value): Appends to input fields, can be used with tags.
  • clear(fieldname): Clears the input field.

 

Clicking Links

There are methods also for clicking links like clickLink(linkText) and seeLink(linkText). 

$this->browse(function (Browser $browser) {
            $browser->visit('/first')
                    ->clickLink('Go to second page')
                    ->assertSee('Go to first page');
        });

This example assumes you have two pages first and second. When clicking the link on the first page it should be taken to second page and see a link with text “Go to first page”.

These are some of the functionalities that laravel dusk provides, but there are many other methods for executing javascript, taking screeshots, dialogues and more. Refer to Laravel Dusk docs to check these method.

 

5 1 vote
Article Rating

What's your reaction?

Excited
0
Happy
0
Not Sure
0
Confused
1

You may also like

Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments