Testing has long been used in development to ensure the reliability of an application. However, popular types of testing do not fully test the full operation of the system when interacting with the user. In this context, the use of browser-based tests helps to verify the real-time performance of a service by detecting errors in the display or operation of the product. In the Laravel ecosystem for such tests, there is a tool adapted to the framework that helps to simulate user actions. Today we will look at how it simulates user actions and what features it provides.
Let’s get started!
According to its documentation, Laravel Dusk provides a simple API for browser automation and testing. And for those people who aren’t familiar, you’ll be pleased to know that it's a wrapper over a pretty well-known tool - ChromeDriver. Today, we will try to understand how to work with this driver and how to connect it to PHP.
The second important thing we want to discuss before we get started is how up-to-date Dusk is. If you look at the latest updates of the library and especially changes in major versions, you will notice that there are almost no updates. On the one hand, this is a disadvantage, as nothing new is introduced, but on the other hand, it can provide stability for your project without the constant need to update everything.
All browser tests are built on the WebDriver tool. Of course, the most popular one is Selenium WebDriver. In comparison, Dusk's differences will be significant, because by default, on the current version, it runs on ChromeWebDriver. However, in the current version of the package from Laravel, there is an option to switch the handling from Chrome to Selenium. This allows you to not get hung up on the implementation and switch the tool as needed.
Laravel Dusk allows you to write Laravel-style browser tests. When writing browser tests, in addition to the standard features from ChromeDriver, you can use some Laravel features, such as working with databases or authorization. Let's see what you can do when writing browser tests.
In order to interact with the browser driver, we need to work with the browse() method, which takes a closure with the Laravel\Dusk\Browser class. This class actually provides an API for working with the browser via PHP.
As far as browser navigation is concerned, it should be noted that Dusk repeats ChromeDriver, only in a beautiful and convenient shell. The methods visit(), back(), forward(), and refresh() are worth mentioning. Based on their names, it becomes clear what navigation elements they work with. After installing Laravel Dusk, you can find the first browser test in the project.
Navigation is already used in it:
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
/**
* A basic browser test example.
*/
public function testBasicExample(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laracasts');
});
}
}
When testing, there is always the question of what data to base tests on. In the context of browser tests, we may need it to check if the elements we show are displayed correctly. Laravel has a whole set of traits just for such needs. And if RefreshDatabase cannot be used in browser tests because of the peculiarities of transactions and HTTP requests, the other two can be used quite well DatabaseMigrations and DatabaseTruncation. The first one recreates the database and performs all migrations after that. The second one will only do TRUNCATE, which will make it run faster.
DatabaseTruncation has the ability to specify which tables to modify. This trait also requires the doctrine/dbal package.
DatabaseMigrations:
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
use DatabaseMigrations;
/**
* A basic browser test example.
*/
public function testBasicExample(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laracasts');
});
}
}
DatabaseTruncation:
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
use DatabaseTruncation;
protected $tablesToTruncate = ['users'];
/**
* A basic browser test example.
*/
public function testBasicExample(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laracasts');
});
}
}
Often, most of the project is closed to guests and open only to users or administrators. To check the view of pages protected from unauthorized eyes we don't need to go to the login form and enter login and password every time. For such cases, especially, Dusk has a method called loginAs(). It allows us to log in for a specific user and takes in either a model that implements the Authenticatable interface or its primary key. The user's session will be saved inside all tests of a single file.
/**
* @throws \Throwable
*/
public function testUserSeeProfile(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/profile')
->loginAs(User::find(1))
->assertSee('Profile');
});
}
When testing an application with browser-based tests, in addition to the boolean result of passing or failing the test, we want to get information about what exactly went correctly and what did not. For this purpose, Laravel Dusk provides methods for saving screenshots, as well as for storing console logs or page source code. In real life, this can be useful for implementing autotests and error output or for forcing page content.
To interact with elements on a page, Dusk has a large set of features inherited from ChromeDriver, such as working with selectors, getting and setting values to different form elements, and clicking buttons and links. An example would be checking if a menu button is clicked to display a drop-down list.
public function testUserClickMenuDropdown(): void
{
$this->browse(function (Browser $browser) {
$user = $this->getUser();
$browser->loginAs($user)
->visit(self::PROFILE_ROUTE)
->waitFor('a#navbarDropdown')
->click('a#navbarDropdown');
$browser->assertVisible('.dropdown-menu.show');
});
}
It's worth noting here that Dusk also allows you to work with custom javascript code and use custom selectors to work with a particular object. To specify a custom selector, you need to specify the dusk="name"
attribute to an object and then that object will be accessible via the '@name'
selector. This can be useful when checking elements that have dynamic class names, or when checking a specific element from a set.
If you need to test hotkeys, scrolls, mouse clicks, and drag and drop items, Laravel Dusk has support for such methods. Among the most popular methods here, I would like to mention the click(), keys(), and dragOffset() methods. They, like many other methods, take a selector as the first parameter. Some web applications have redesigned the right mouse click in the browser. To test such a case, there is the rightClick() method.
public function testUserLogin(): void
{
$this->browse(function (Browser $browser) {
$user = $this->getUser();
$browser->visit(self::LOGIN_ROUTE)
->keys('form input[type="email"]', $user->email)
->keys('form input[type="password"]', 'password')
->press('Login')
->assertPathIs(self::HOME_ROUTE);
});
}
When interacting with forms or code blocks, it is convenient to use the scope of selectors using the with() method. It allows you to limit the scope within a single selector, which allows you to make simpler and more readable test code. In this case, the previous example can be made like this:
public function testUserLoginWith(): void
{
$this->browse(function (Browser $browser) {
$browser->visit(self::LOGIN_ROUTE)
->with('form', function (Browser $browser) {
$user = $this->getUser();
$browser->keys('input[type="email"]', $user->email)
->keys('input[type="password"]', 'password')
->press('Login')
->assertPathIs(self::HOME_ROUTE);
});
});
}
When interacting with a large number of elements, this method will be preferred.
In some of the examples above, you may have noticed methods of waiting for some page elements by their selector. This is an essential tool when testing pages with dynamic content. When loading content with Javascript, we can wait for it using a bunch of methods such as waitFor(), waitForText(), waitUntilEnabled(). We can also use a group of these methods to interact with Javascript on a page, for example, wait for a value to be substituted into a component with waitUntilVue() or wait for an event with waitForEvent().
Laravel Dusk has a large range of assertion options, you can see the full list here. Some of the most commonly used ones are assertSee(), assertValue(), assertPathIs(), assertVisible(). Many other methods are part of those mentioned to improve code readability and syntactic sugar.
When running complex browser tests, it can be necessary to go through multiple pages to validate a user action. To avoid an overly complex structure, Dusk has pages. Each page has url(), assert(), and elements() methods. And while the first one, based on the name, gives a path string to that page in the response, the second one checks if we are actually on that page, for example with a simple check:
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url());
}
Then, the third has the most obvious behavior. The elements() method stores macros for the selectors, typing the selectors, preventing simple mistakes from being made, and allowing work by reusing the described macros. For an example of how pages work, we'll rewrite the testUserLoginWith()
public function elements(): array
{
return [
'@email' => 'input[type="email"]',
'@password' => 'input[type="password"]',
'@submit' => 'button[type="submit"]',
];
}
public function testUserLoginWith(): void
{
$this->browse(function (Browser $browser) {
$user = $this->getUser();
$browser->visit(new Login())
->keys('@email', $user->email)
->keys('@password', 'password')
->press('@submit')
->assertPathIs(self::HOME_ROUTE);
});
}
If you compare this listing to our previous one, you'll notice how much more readable the test has become.
Components are very similar to Pages, but they are used for interface components, such as the site header or modal window. The only difference is that instead of the url() method, components have a selector() method.
As you and I have seen, Dusk has a large number of features, including making it easy to write tests, not just working with ChromeDriver. Dusk developers' focus on code readability has resulted in the fact that many not-so-pleasant parts of writing tests, such as working with databases, authentication, or repeating a large number of selectors, are solved and we only have to write the test itself. Browser response logging features help when setting up tests and also when setting up test execution during the deployment. In conclusion, I would like to say that Laravel Dusk is a really handy tool that allows you to quickly make the necessary set of tests.
Lead image by Mohammed Rahmani on Unsplash