Running Laravel Dusk E2E tests with Selenium in GitLab CI
5 min read

End-to-end (E2E) testing is crucial for ensuring application quality by simulating real user scenarios. Laravel Dusk provides an elegant API for browser automation and testing. However, running these tests in a CI/CD environment like GitLab can be tricky.
This article will guide you through setting up a GitLab CI job to run your Laravel Dusk tests using a standalone Selenium service, ensuring your application works as expected from the user’s perspective.
Environment Configuration
First, we need to configure our Laravel application to communicate with the services inside the GitLab CI environment. We’ll use a dedicated .env
file for this, which will be copied in our CI job. Let’s call it .env.ci.dusk.chrome
.
# .env.ci.dusk.chrome
# ...
APP_URL=http://build:8000
DUSK_BROWSER=chrome
DUSK_DRIVER_URL=http://selenium:4444/wd/hub
# ...
DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=dusk_db
DB_USERNAME=root
DB_PASSWORD=root
Key points here:
APP_URL
: Points tohttp://build:8000
. In GitLab CI,build
is the default hostname of the job container. We’ll runphp artisan serve
on port8000
.DUSK_DRIVER_URL
: This tells Dusk to connect to the Selenium service, which we’ll nameselenium
.DB_HOST
: This is set tomysql
, the hostname of our database service in GitLab CI.
GitLab CI Job (.gitlab-ci.yml
)
Now, let’s define the GitLab CI job that will execute our Dusk tests.
# .gitlab-ci.yml
stages:
- test
# ...
variables:
FF_NETWORK_PER_BUILD: 1 # Enables network communication between job and services
# ...
dusk:
stage: test
image: lorisleiva/laravel-docker:8.4
services:
- mysql:latest
- name: selenium/standalone-chromium:latest
alias: selenium
variables:
MYSQL_DATABASE: dusk_db
MYSQL_ROOT_PASSWORD: root
before_script:
- echo 'memory_limit = 512M' >> /usr/local/etc/php/conf.d/docker-php-memory-limit.ini
- cp .env.ci.dusk.chrome .env
script:
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts --ignore-platform-reqs
- php artisan key:generate --force
- php artisan migrate --force
- npm install
- npm run build
- php artisan serve --host=0.0.0.0 --port=8000 &
- |
for i in {1..30}; do
if curl -s http://build:8000 > /dev/null; then
echo "Server is up and running!"
break
fi
echo "Waiting for server to start... ($i/30)"
sleep 2
done
- php artisan dusk --no-ansi
artifacts:
when: always
paths:
- tests/Browser/screenshots
- tests/Browser/console
- storage/logs
expire_in: 7 day
# ... (cache and rules omitted for brevity)
Let’s break down this job:
FF_NETWORK_PER_BUILD: 1
: This is a crucial GitLab feature flag that creates a dedicated network for each job, allowing the job container and its services to communicate via hostnames.image
: We uselorisleiva/laravel-docker:8.4
, a convenient image containing PHP, Composer, Node, and other necessary tools.services
: We define two services.mysql:latest
for our database, andselenium/standalone-chromium:latest
for running Chrome. We give the Selenium service analias: selenium
so we can reach it athttp://selenium:4444
.before_script
: We copy our CI-specific.env
file.script
:- We install dependencies (
composer
andnpm
). - We set up the application (
key:generate
,migrate
). - We build frontend assets (
npm run build
). php artisan serve --host=0.0.0.0 --port=8000 &
starts the Laravel development server in the background.--host=0.0.0.0
is essential to make it accessible from outside the container.- The
for
loop is a simple health check. It waits for the application server to become responsive before proceeding. php artisan dusk
finally runs our tests.
- We install dependencies (
artifacts
: We save screenshots, console logs, and application logs from failed tests for debugging.
Configuring DuskTestCase.php
To make Dusk connect to the remote Selenium driver instead of a local ChromeDriver, we need to modify tests/DuskTestCase.php
. This setup also allows for running tests with Firefox by changing the DUSK_BROWSER
environment variable.
// tests/DuskTestCase.php
<?php
namespace Tests;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Firefox\FirefoxOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Laravel\Dusk\TestCase as BaseTestCase;
use PHPUnit\Framework\Attributes\BeforeClass;
abstract class DuskTestCase extends BaseTestCase
{
/**
* Prepare for Dusk test execution.
*/
#[BeforeClass]
public static function prepare(): void
{
// We don't need to start a local chromedriver when using a remote Selenium service.
// The default behavior is commented out.
// if (! static::runningInSail() || $isCI) {
// static::startChromeDriver(['--port=9515']);
// }
}
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
{
$browser = $_ENV['DUSK_BROWSER'] ?? env('DUSK_BROWSER', 'chrome');
$seleniumUrl = $_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515';
if ($browser === 'firefox') {
$ffArgs = collect([])
->unless($this->hasHeadlessDisabled(), fn ($items) => $items->merge(['-headless']))
->all();
$options = (new FirefoxOptions())->addArguments($ffArgs);
$capabilities = DesiredCapabilities::firefox()
->setCapability(FirefoxOptions::CAPABILITY, $options)
->setCapability('acceptInsecureCerts', true);
return RemoteWebDriver::create($seleniumUrl, $capabilities, 60000, 120000);
}
$chArgs = collect([
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
'--disable-search-engine-choice-screen',
'--disable-smooth-scrolling',
'--ignore-certificate-errors',
'--allow-insecure-localhost',
'--disable-dev-shm-usage',
'--no-sandbox',
])->unless($this->hasHeadlessDisabled(), fn ($items) => $items->merge([
'--headless=new',
'--window-size=1920,1080',
]))->all();
$options = (new ChromeOptions())->addArguments($chArgs);
$capabilities = DesiredCapabilities::chrome()
->setCapability(ChromeOptions::CAPABILITY, $options)
->setCapability('acceptInsecureCerts', true);
return RemoteWebDriver::create($seleniumUrl, $capabilities, 60000, 120000);
}
}
The key changes are:
- The
prepare
method is emptied to prevent Dusk from trying to start its own ChromeDriver. - The
driver
method is overridden to create aRemoteWebDriver
instance. It reads theDUSK_DRIVER_URL
from the environment and connects to our Selenium service. - It includes several useful Chrome arguments for running in a containerized environment, like
--no-sandbox
and--disable-dev-shm-usage
.
Example Dusk Test
Here is a simple login test to demonstrate the setup. It uses Dusk’s Page Objects and assertions to verify the login functionality.
// tests/Browser/LoginTest.php
<?php
namespace Tests\Browser;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTruncation;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\Browser\Pages\LoginPage;
use Tests\DuskTestCase;
class LoginTest extends DuskTestCase
{
use DatabaseTruncation;
public function test_users_can_login(): void
{
$this->browse(function (Browser $browser) {
$user = User::factory()->create([
'password' => Hash::make('password'),
]);
$browser->visit(new LoginPage())
->waitFor('@login-page', 10)
->type('@email', $user->email)
->type('@password', 'password')
->click('@submit')
->waitFor('@dashboard-page', 10)
->assertPathIs('/dashboard')
->assertAuthenticated();
});
}
}
With this configuration, your Dusk tests will run seamlessly in your GitLab CI pipeline, providing you with fast and reliable feedback on your application’s E2E behavior.
- #laravel
- #dusk
- #selenium
- #gitlab
- #ci-cd
- #e2e-testing