Most developers know that testing your code is A Good Thing. We’re supposed to do it. We likely have an idea of why it’s good, and we might’ve even read some tutorials about how it’s supposed to work.
But the gap between knowing why you should test and knowing how to test is wide. Thankfully, tools like PHPUnit, Mockery, and PHPSpec provide an incredible number of options for testing in PHP—but it can still be pretty overwhelming to get everything set up.
Out of the box, Laravel comes with baked-in integrations to PHPUnit (unit testing), Mockery (mocking), and Faker (creating fake data for seeding and testing). It also provides its own simple and powerful suite of application testing tools, which allow you to “crawl” your site’s URIs, submit forms, check HTTP status codes, and validate and assert against JSON. It also provides a robust frontend testing framework called Dusk that can even interact with your JavaScript applications and test against them. In case this hasn’t made it clear, we’re going to cover a lot of ground in this chapter.
To make it easy for you to get started, Laravel’s testing setup comes with sample application test that can run successfully the moment you create a new app. That means you don’t have to spend any time configuring your testing environment, and that’s one less barrier to writing your tests.
Tests in Laravel live in the tests folder. There are two files in the root: TestCase.php, which is the base root test which all of your tests will extend, and CreatesApplication.php, a trait (imported by TestCase.php) which allows any class to boot a sample Laravel application for testing.
There are also two subfolders: Features, for tests that cover the interaction between multiple units, and Unit, for tests that are intended to cover just one unit of your code (class, module, function, etc.). Each of these folders contains an ExampleTest.php file, each of which has a single sample test inside it, ready to run.
In projects running versions of Laravel prior to 5.4, there will be only two files in the tests directory: ExampleTest.php, your sample test, and TestCase.php, your base test.
Additionally, if your app is pre-5.4, the syntax in all of the examples in this chapter will not be quite right. All the ideas are the same, but the syntax is a bit different across the board. You can learn more in the Laravel 5.3 testing docs. Here are the four biggest changes:
In 5.3 and before, you’re not creating response objects; instead, you’re just calling methods on $this
, and the test class stores the responses. So, $response = $this->get('people')
in 5.4+ would look like $this->get('people')
in 5.3 and earlier.
Many of the assertions have been renamed in small ways in 5.4+ to make them look more like PHPUnit’s normal assertion names; for example, assertSee()
instead of see()
.
Some of the “crawling” methods that in 5.4+ have been extracted out to browser-kit-testing
were built into the core in previous versions.
Dusk didn’t exist prior to 5.4.
Because testing prior to 5.4 was so different, I’ve made the testing chapter for the first edition of this book available as a free PDF. If you’re working with 5.3 or earlier, I’d recommend skipping this chapter in the book and using this PDF of the testing chapter from the first edition instead.
The ExampleTest
in your Unit directory contains one simple assertion: $this
->
assertTrue(true)
. Anything in your unit tests is likely to be relatively simple PHPUnit syntax (asserting that values are equal or different, looking for entries in arrays, checking Booleans, etc.), so there’s not much to learn there.
If you’re not yet familiar with PHPUnit, most of our assertions will be run on the $this
object with this syntax:
$this
->
assertWHATEVER
(
$expected
,
$real
);
So, for example, if we’re asserting that two variables should be equal, we’ll pass it first our expected result, and second the actual outcome of the object or system we’re testing:
$multiplicationResult
=
$myCalculator
->
multiply
(
5
,
3
);
$this
->
assertEqual
(
15
,
$multiplicationResult
);
As you can see in Example 12-1, the ExampleTest
in the Feature directory makes a simulated HTTP request to the page at the root path of your application and checks that its HTTP status is 200 (successful). If it is, it’ll pass; if not, it’ll fail. Unlike your average PHPUnit test, we’re running these assertions on the TestResponse
object that’s returned when we make test HTTP calls.
<?
php
namespace
TestsFeature
;
use
TestsTestCase
;
use
IlluminateFoundationTestingRefreshDatabase
;
class
ExampleTest
extends
TestCase
{
/**
* A basic test example
*
* @return void
*/
public
function
testBasicTest
()
{
$response
=
$this
->
get
(
'/'
);
$response
->
assertStatus
(
200
);
}
}
To run the tests, run ./vendor/bin/phpunit
on the command line from the root folder of your application. You should see something like the output in Example 12-2.
PHPUnit 7.3.5 by Sebastian Bergmann and contributors. ..2
/2
(
100%)
Time:139
ms, Memory: 12.00MB OK(
2
test
,2
assertions)
You just ran your first Laravel application test! Those two dots indicate that you have two passing tests. As you can see, you’re set up out of the box not only with a functioning PHPUnit instance, but also a full-fledged application testing suite that can make mock HTTP calls and test your application’s responses. Further, you’ll soon learn that you have easy access to a fully featured DOM crawler (“A Quick Introduction to BrowserKit Testing”) and a regression testing tool with full JavaScript support (“Testing with Dusk”).
In case you’re not familiar with PHPUnit, let’s take a look at what it’s like to have a test fail. Instead of modifying the previous test, we’ll make our own. Run php artisan make:test FailingTest
. This will create the file tests/Feature/FailingTest.php; you can modify its testExample()
method to look like Example 12-3.
public
function
testExample
()
{
$response
=
$this
->
get
(
'/'
);
$response
->
assertStatus
(
301
);
}
As you can see, it’s the same as the test we ran previously, but we’re now testing against the wrong status. Let’s run PHPUnit again.
If you want your test to be generated in the Unit
directory instead of the Feature
directory, pass the --unit
flag:
php artisan make:test SubscriptionTest --unit
Whoops! This time the output will probably look a bit like Example 12-4.
PHPUnit 7.3.5 by Sebastian Bergmann and contributors. .F. 3 / 3 (100%) Time: 237 ms, Memory: 12.00MB There was 1 failure: 1) TestsFeatureFailingTest::testExample Expected status code 301 but received 200. Failed asserting that false is true. /path-to-your-app/vendor/.../Foundation/Testing/TestResponse.php:124 /path-to-your-app/tests/Feature/FailingTest.php:20 FAILURES! Tests: 3, Assertions: 3, Failures: 1.
Let’s break this down. Last time there were only two dots, representing the two passing tests, but this time there’s an F
between them indicating that one of the three tests run here has failed.
Then, for each error, we see the test name (here, FailingTest::testExample
), the error message (Expected status code...
), and a full stack trace, so we can see what was called. Since this was an application test, the stack trace just shows us that it was called via the TestResponse
class, but if this were a unit or feature test, we’d see the entire call stack of the test.
Now that we’ve run both a passing test and a failing test, it’s time for you to learn more about Laravel’s testing environment.
By default, Laravel’s testing system will run any files in the tests directory whose names end with the word Test. That’s why tests/ExampleTest.php was run by default.
If you’re not familiar with PHPUnit, you might not know that only the methods in your tests with names that start with the word test
will be run—or methods with a @test
documentation block, or docblock. See Example 12-5 for which methods will and won’t run.
class
NamingTest
{
public
function
test_it_names_things_well
()
{
// Runs as "It names things well"
}
public
function
testItNamesThingsWell
()
{
// Runs as "It names things well"
}
/** @test */
public
function
it_names_things_well
()
{
// Runs as "It names things well"
}
public
function
it_names_things_well
()
{
// Doesn't run
}
}
Any time a Laravel application is running, it has a current “environment” name that represents the environment it’s running in. This name may be set to local
, staging
, production
, or anything else you want. You can retrieve this by running app()->environment()
, or you can run if (app()->
environment('local'))
or something similar to test whether the current environment matches the passed name.
When you run tests, Laravel automatically sets the environment to testing
. This means you can test for if (app()->environment('testing'))
to enable or disable certain behaviors in the testing environment.
Additionally, Laravel doesn’t load the normal environment variables from .env for testing. If you want to set any environment variables for your tests, edit phpunit.xml and, in the <php>
section, add a new <env>
for each environment variable you want to pass in—for example, <env name="DB_CONNECTION" value="sqlite"/>
.
Before we get into the methods you can use for testing, you need to know about the four testing traits you can pull into any test class.
IlluminateFoundationTestingRefreshDatabase
is imported at the top of every newly generated test file, and it’s the most commonly used database migration trait. This trait was introduced in Laravel 5.5 and is only available in projects running on that version or later.
The point of this, and the other database traits, is to ensure your database tables are correctly migrated at the start of each test.
RefreshDatabase
takes two steps to do this. First, it runs your migrations on your test database once at the beginning of each test run (when you run phpunit
, not for each individual test method). And second, it wraps each individual test method in a database transaction and rolls back the transaction at the end of the test.
That means you have your database migrated for your tests and cleared out fresh after each test runs, without having to run your migrations again before every test—making this the fastest possible option. When in doubt, stick with this.
If you import IlluminateFoundationTestingWithoutMiddleware
into your test class, it will disable all middleware for any test in that class. This means you won’t have to worry about the authentication middleware, or CSRF protection, or anything else that might be useful in the real application but distracting in a test.
If you’d like to disable middleware for just a single method instead of the entire test class, call $this->withoutMiddleware()
at the top of the method for that test.
If you import the IlluminateFoundationTestingDatabaseMigrations
trait instead of the RefreshDatabase
trait, it will run your entire set of database migrations fresh before each test. Laravel makes this happen by running php artisan migrate:fresh
in the setUp()
method before every test runs.
IlluminateFoundationTestingDatabaseTransactions
, on the other hand, expects your database to be properly migrated before your tests start. It wraps every test in a database transaction, which it rolls back at the end of each test. This means that, at the end of each test, your database will be returned to the exact same state it was in prior to the test.
With simple unit tests, you almost don’t need any of these traits. You may reach for database access or inject something out of the container, but it’s very likely that unit tests in your applications won’t rely on the framework very much. Take a look at Example 12-6 for an example of what a simple test might look like.
class
GeometryTest
extends
TestCase
{
public
function
test_it_calculates_area
()
{
$square
=
new
Square
;
$square
->
sideLength
=
4
;
$calculator
=
new
GeometryCalculator
;
$this
->
assertEquals
(
16
,
$calculator
->
area
(
$square
));
}
Obviously, this is a bit of a contrived example. But you can see here that we’re testing a single class (GeometryCalculator
) and its single method (area()
), and we’re doing so without worrying about the entire Laravel application.
Some unit tests might be testing something that technically is connected to the framework—for example, Eloquent models—but you can still test them without worrying about the framework. For example, in Example 12-7, we’ll use Package::make()
instead of Package::create()
so the object is created and evaluated in memory without ever hitting the database.
class
PopularityTest
extends
TestCase
{
use
RefreshDatabase
;
public
function
test_votes_matter_more_than_views
()
{
$package1
=
Package
::
make
([
'votes'
=>
1
,
'views'
=>
0
]);
$package2
=
Package
::
make
([
'votes'
=>
0
,
'views'
=>
1
]);
$this
->
assertTrue
(
$package1
->
popularity
>
$package2
->
popularity
);
}
Some people may call this an integration or feature test, since this “unit” will likely touch the database in actual usage and it’s connected to the entire Eloquent codebase. The most important point is that you can have simple tests that test a single class or method, even when the objects under test are framework-connected.
All of this said, it’s still going to be more likely that your tests—especially as you first get started—are broader and more at the “application” level. Accordingly, for the rest of the chapter we’re going to dig deeper into application testing.
In “Testing Basics” we saw that, with a few lines of code, we can “request” URIs in our application and actually check the status of the response. But how can PHPUnit request pages as if it were a browser?
Any application tests should extend the TestCase
class (tests/TestCase.php) that’s included with Laravel by default. Your application’s TestCase
class will extend the abstract IlluminateFoundationTestingTestCase
class, which brings in quite a few goodies.
The first thing the two TestCase
classes (yours and its abstract parent) do is handle booting the Illuminate application instance for you, so you have a fully bootstrapped application available. They also “refresh” the application between each test, which means they’re not entirely recreating the application between tests, but rather making sure you don’t have any data lingering.
The parent TestCase
also sets up a system of hooks that allow callbacks to be run before and after the application is created, and imports a series of traits that provide you with methods for interacting with every aspect of your application. These traits include InteractsWithContainer
, MakesHttpRequests
, and InteractsWithConsole
, and they bring in a broad variety of custom assertions and testing methods.
As a result, your application tests have access to a fully bootstrapped application instance and application-test-minded custom assertions, with a series of simple and powerful wrappers around each to make them easy to use.
That means you can write $this->get('/')->assertStatus(200)
and know that your application is actually behaving as if it were responding to a normal HTTP request, and that the response is being fully generated and then checked as a browser would check it. It’s pretty powerful stuff, considering how little work you had to do to get it running.
Let’s take a look at our options for writing HTTP-based tests. You’ve already seen $this->get('/')
, but let’s dive deeper into how you can use that call, how you can assert against its results, and what other HTTP calls you can make.
At the very basic level, Laravel’s HTTP testing allows you to make simple HTTP requests (GET
, POST
, etc.) and then make simple assertions about their impact or response.
There are more tools we’ll cover later (in “A Quick Introduction to BrowserKit Testing” and “Testing with Dusk”) that allow for more complex page interactions and assertions, but let’s start at the base level. Here are the calls you can make:
$this->get($uri, $headers = [])
$this->post($uri, $data = [], $headers = [])
$this->put($uri, $data = [], $headers = [])
$this->patch($uri, $data = [], $headers = [])
$this->delete($uri, $data = [], $headers = [])
$this->option($uri, $data = [], $headers = [])
These methods are the basis of the HTTP testing framework. Each takes at least a URI (usually relative) and headers, and all but get()
also allow for passing data along with the request.
And, importantly, each returns a $response
object that represents the HTTP response. This response object is almost exactly the same as an Illuminate Response
object, the same thing we return out of our controllers. However, it’s actually an instance of IlluminateFoundationTestingTestResponse
, which wraps a normal Response
with some assertions for testing.
Take a look at Example 12-8 to see a common usage of post()
and a common response assertion.
public
function
test_it_stores_new_packages
()
{
$response
=
$this
->
post
(
route
(
'packages.store'
),
[
'name'
=>
'The greatest package'
,
]);
$response
->
assertOk
();
}
In most examples like Example 12-8, you’ll also test that the record exists in the database and shows up on the index page, and maybe that it doesn’t test successfully unless you define the package author and are logged in. But don’t worry, we’ll get to all of that. For now, you can make calls to your application routes with many different verbs and make assertions against both the response and the state of your application afterward. Great!
You can also do all of the same sorts of HTTP tests with your JSON APIs. There are convenience methods for that, too:
$this->getJson($uri, $headers = [])
$this->postJson($uri, $data = [], $headers = [])
$this->putJson($uri, $data = [], $headers = [])
$this->patchJson($uri, $data = [], $headers = [])
$this->deleteJson($uri, $data = [], $headers = [])
$this->optionJson($uri, $data = [], $headers = [])
These methods work just the same as the normal HTTP call methods, except they also add JSON-specific Accept
, CONTENT_LENGTH
, and CONTENT_TYPE
headers. Take a look at Example 12-9 to see an example.
public
function
test_the_api_route_stores_new_packages
()
{
$response
=
$this
->
postJSON
(
route
(
'api.packages.store'
),
[
'name'
=>
'The greatest package'
,
],
[
'X-API-Version'
=>
'17'
]);
$response
->
assertOk
();
}
There are 40 assertions available on the $response
object in Laravel 5.8, so I’ll refer you to the testing docs for details on all of them. Let’s look at a few of the most important and most common ones:
$response->assertOk()
Asserts that the response’s status code is 200:
$response
=
$this
->
get
(
'terms'
);
$response
->
assertOk
();
$response->assertSuccessful()
While assertOk()
asserts that the code is a 200, assertSuccessful()
checks if the code is any anything in the 200 group:
$response
=
$this
->
post
(
'articles'
,
[
'title'
=>
'Testing Laravel'
,
'body'
=>
'My article about testing Laravel'
,
]);
// Assuming this returns 201 CREATED...
$response
->
assertSuccessful
();
$response->assertUnauthorized()
Asserts that the response’s status code is 401:
$response
=
$this
->
patch
(
'settings'
,
[
'password'
=>
'abc'
]);
$response
->
assertUnauthorized
();
$response->assertForbidden()
Asserts that the response’s status code is 403:
$response
=
$this
->
actingAs
(
$normalUser
)
->
get
(
'admin'
);
$response
->
assertForbidden
();
$response->assertNotFound()
Asserts that the response’s status code is 404:
$response
=
$this
->
get
(
'posts/first-post'
);
$response
->
assertNotFound
();
$response->assertStatus($status)
Asserts that the response’s status code is equal to the provided $status
:
$response
=
$this
->
get
(
'admin'
);
$response
->
assertStatus
(
401
);
// Unauthorized
$response->assertSee($text)
and $response->assertDontSee($text)
Asserts that the response contains (or doesn’t contain) the provided $text
:
$package
=
factory
(
Package
::
class
)
->
create
();
$response
=
$this
->
get
(
route
(
'packages.index'
));
$response
->
assertSee
(
$package
->
name
);
$response->assertJson(_array $json)
Asserts that the passed array is represented (in JSON format) in the returned JSON:
$this
->
postJson
(
route
(
'packages.store'
),
[
'name'
=>
'GreatPackage2000'
]);
$response
=
$this
->
getJson
(
route
(
'packages.index'
));
$response
->
assertJson
([
'name'
=>
'GreatPackage2000'
]);
$response->assertViewHas($key, $value = null)
Asserts that the view on the visited page had a piece of data available at $key
, and optionally checks that the value of that variable was $value
:
$package
=
factory
(
Package
::
class
)
->
create
();
$response
=
$this
->
get
(
route
(
'packages.show'
));
$response
->
assertViewHas
(
'name'
,
$package
->
name
);
$response->assertSessionHas($key, $value = null)
Asserts that the session has data set at $key
, and optionally checks that the value of that data is $value
:
$response
=
$this
->
get
(
'beta/enable'
);
$response
->
assertSessionHas
(
'beta-enabled'
,
true
);
$response->assertSessionHasInput($key, $value = null)
Asserts that the given keys and values are flashed in the session array input. This is helpful when testing the validation error returns the correct old values.
$response
=
$this
->
post
(
'users'
,
[
'name'
=>
'Abdullah'
]);
// Assuming it errored, check that the entered name is flashed;
$response
->
assertSessionHasInput
([
'name'
=>
'Abdullah'
]);
$response->assertSessionHasErrors()
With no parameters, asserts that there’s at least one error set in Laravel’s special errors
session container. Its first parameter can be an array of key/value pairs that define the errors that should be set and its second parameter can be the string format that the checked errors should be formatted against, as demonstrated here:
// Assuming the "/form" route requires an email field, and we're
// posting an empty submission to it to trigger the error
$response
=
$this
->
post
(
'form'
,
[]);
$response
->
assertSessionHasErrors
();
$response
->
assertSessionHasErrors
([
'email'
=>
'The email field is required.'
,
]);
$response
->
assertSessionHasErrors
(
[
'email'
=>
'<p>The email field is required.</p>'
],
'<p>:message</p>'
);
If you’re working with named error bags, you can pass the error bag name as the third parameter.
$response->assertCookie($name, $value = null)
Asserts that the response contains a cookie with name $name
, and optionally checks that its value is $value
:
$response
=
$this
->
post
(
'settings'
,
[
'dismiss-warning'
]);
$response
->
assertCookie
(
'warning-dismiss'
,
true
);
$response->assertCookieExpired($name)
Asserts that the response contains a cookie with name $name
and that it is expired:
$response
->
assertCookieExpired
(
'warning-dismiss'
);
$response->assertCookieNotExpired($name)
Asserts that the response contains a cookie with name $name
and that it is not expired:
$response
->
assertCookieNotExpired
(
'warning-dismiss'
);
$response->assertRedirect($uri)
Asserts that the requested route returns a redirect to the given URI:
$response
=
$this
->
post
(
route
(
'packages.store'
),
[
'email'
=>
'invalid'
]);
$response
->
assertRedirect
(
route
(
'packages.create'
));
For each of these assertions, you can assume that there are many related assertions I haven’t listed here. For example, in addition to assertSessionHasErrors()
there are also assertSessionHasNoErrors()
and assertSessionHasErrorsIn()
assertions; as well as assertJson()
, there are also assertJsonCount()
, assertJsonFragment()
, assertJsonMissing()
, assertJsonMissingExact()
, assertJsonStructure()
, and assertJsonValidationErrors()
assertions. Again, take a look at the docs and make yourself familiar with the whole list.
One piece of your application it’s common to test with application tests is authentication and authorization. Most of the time your needs will be met with the actingAs()
chainable method, which takes a user (or other Authenticatable
object, depending on how your system is set up), as you can see in Example 12-10.
public
function
test_guests_cant_view_dashboard
()
{
$user
=
factory
(
User
::
class
)
->
states
(
'guest'
)
->
create
();
$response
=
$this
->
actingAs
(
$user
)
->
get
(
'dashboard'
);
$response
->
assertStatus
(
401
);
// Unauthorized
}
public
function
test_members_can_view_dashboard
()
{
$user
=
factory
(
User
::
class
)
->
states
(
'member'
)
->
create
();
$response
=
$this
->
actingAs
(
$user
)
->
get
(
'dashboard'
);
$response
->
assertOk
();
}
public
function
test_members_and_guests_cant_view_statistics
()
{
$guest
=
factory
(
User
::
class
)
->
states
(
'guest'
)
->
create
();
$response
=
$this
->
actingAs
(
$guest
)
->
get
(
'statistics'
);
$response
->
assertStatus
(
401
);
// Unauthorized
$member
=
factory
(
User
::
class
)
->
states
(
'member'
)
->
create
();
$response
=
$this
->
actingAs
(
$member
)
->
get
(
'statistics'
);
$response
->
assertStatus
(
401
);
// Unauthorized
}
public
function
test_admins_can_view_statistics
()
{
$user
=
factory
(
User
::
class
)
->
states
(
'admin'
)
->
create
();
$response
=
$this
->
actingAs
(
$user
)
->
get
(
'statistics'
);
$response
->
assertOk
();
}
It’s common to use model factories (discussed in “Model Factories”) in testing, and model factory states make tasks like creating users with different access levels simple.
If you’d like to set session variables on your requests, you can also chain withSession()
:
$response
=
$this
->
withSession
([
'alert-dismissed'
=>
true
,
])
->
get
(
'dashboard'
);
If you’d prefer to set your request headers fluently, you can chain withHeaders()
:
$response
=
$this
->
withHeaders
([
'X-THE-ANSWER'
=>
'42'
,
])
->
get
(
'the-restaurant-at-the-end-of-the-universe'
);
Usually, an exception that’s thrown inside your application when you’re making HTTP calls will be captured by Laravel’s exception handler and processed as it would be in normal application. So, the test and route in Example 12-11 would still pass, since the exception would never bubble up the whole way to our test.
// routes/web.php
Route
::
get
(
'has-exceptions'
,
function
()
{
throw
new
Exception
(
'Stop!'
);
});
// tests/Feature/ExceptionsTest.php
public
function
test_exception_in_route
()
{
$this
->
get
(
'/has-exceptions'
);
$this
->
assertTrue
(
true
);
}
In a lot of cases, this might make sense; maybe you’re expecting a validation exception and you want it to be caught like it would normally be by the framework.
But if you want to temporarily disable the exception handler, that’s an option; just run $this->withoutExceptionHandling()
, as shown in Example 12-12.
// tests/Feature/ExceptionsTest.php
public
function
test_exception_in_route
()
{
// Now throws an error
$this
->
withoutExceptionHandling
();
$this
->
get
(
'/has-exceptions'
);
$this
->
assertTrue
(
true
);
}
And if for some reason you need to turn it back on (maybe you turned it off in setUp()
but want it back on for just one test), you can run $this
->
withExceptionHandling()
.
In Laravel 5.8+, you can easily dump out the headers with dumpHeaders()
or the body with dump()
. It’s also possible to do this before 5.8, but it’s a bit more work.
$response
=
$this
->
get
(
'/'
);
// Before 5.8
dump
(
$response
->
headers
->
all
());
dump
(
json_decode
(
$response
->
getContent
()));
// json
dump
(
$response
->
getContent
());
// not json
// In 5.8+
$response
->
dumpHeaders
();
$response
->
dump
();
Often, the effect we want to test for after our tests have run is in the database. Imagine you want to test that the “create package” page works correctly. What’s the best way? Make an HTTP call to the “store package” endpoint and then assert that that package exists in the database. It’s easier and safer than inspecting the resulting “list packages” page.
We have two primary assertions for the database: $this->assertDatabaseHas()
and $this->assertDatabaseMissing()
. For both, pass the table name as the first parameter, the data you’re looking for as the second, and, optionally, the specific database connection you want to test as the third.
Take a look at Example 12-13 to see how you might use them.
public
function
test_create_package_page_stores_package
()
{
$this
->
post
(
route
(
'packages.store'
),
[
'name'
=>
'Package-a-tron'
,
]);
$this
->
assertDatabaseHas
(
'packages'
,
[
'name'
=>
'Package-a-tron'
]);
}
As you can see, the second (data) parameter of assertDatabaseHas()
is structured like a SQL WHERE
statement—you pass a key and a value (or multiple keys and values), and then Laravel looks for any records in the specified database table that match your key(s) and value(s).
As always, assertDatabaseMissing()
is the inverse.
Model factories are amazing tools that make it easy to seed randomized, well-structured database data for testing (or other purposes). You’ve already seen them in use in several examples in this chapter.
We’ve already covered them in depth, so check out “Model Factories” to learn more.
When testing Laravel systems, you’ll often want to pause their true function for the duration of the testing and instead write tests against what has happened to those systems. You can do this by “faking” different facades, such as Event
, Mail
, and Notification
. We’ll talk more about what fakes are in “Mocking”, but first, let’s look at some examples. All of the following features in Laravel have their own set of assertions you can make after faking them, but you can also just choose to fake them to restrict their effects.
Let’s use event fakes as our first example of how Laravel makes it possible to mock its internal systems. There are likely going to be times when you want to fake events just for the sake of suppressing their actions. For example, suppose your app pushes notifications to Slack every time a new user signs up. You have a “user signed up” event that’s dispatched when this happens, and it has a listener that notifies a Slack channel that a user has signed up. You don’t want those notifications to go to Slack every time you run your tests, but you might want to assert that the event was sent, or the listener was triggered, or something else. This is one reason for faking certain aspects of Laravel in our tests: to pause the default behavior and instead make assertions against the system we’re testing.
Let’s take a look at how to suppress these events by calling the fake()
method on IlluminateSupportFacadesEvent
, as shown in Example 12-14.
public
function
test_controller_does_some_thing
()
{
Event
::
fake
();
// Call controller and assert it does whatever you want without
// worrying about it pinging Slack
}
Once we’ve run the fake()
method, we can also call special assertions on the Event
facade: namely, assertDispatched()
and assertNotDispatched()
. Take a look at Example 12-15 to see them in use.
public
function
test_signing_up_users_notifies_slack
()
{
Event
::
fake
();
// Sign user up
Event
::
assertDispatched
(
UserJoined
::
class
,
function
(
$event
)
use
(
$user
)
{
return
$event
->
user
->
id
===
$user
->
id
;
});
// Or sign multiple users up and assert it was dispatched twice
Event
::
assertDispatched
(
UserJoined
::
class
,
2
);
// Or sign up with validation failures and assert it wasn't dispatched
Event
::
assertNotDispatched
(
UserJoined
::
class
);
}
Note that the (optional) closure we’re passing to assertDispatched()
makes it so we’re not just asserting that the event was dispatched, but also that the dispatched event contains certain data.
The Bus
facade, which represents how Laravel dispatches jobs, works just like Event
. You can run fake()
on it to disable the impact of your jobs, and after faking it you can run assertDispatched()
or assertNotDispatched()
.
The Queue
facade represents how Laravel dispatches jobs when they’re pushed up to queues. Its available methods are assertedPushed()
, assertPushedOn()
, and assertNotPushed()
.
Take a look at Example 12-16 to see how to use both.
public
function
test_popularity_is_calculated
()
{
Bus
::
fake
();
// Synchronize package data...
// Assert a job was dispatched
Bus
::
assertDispatched
(
CalculatePopularity
::
class
,
function
(
$job
)
use
(
$package
)
{
return
$job
->
package
->
id
===
$package
->
id
;
}
);
// Assert a job was not dispatched
Bus
::
assertNotDispatched
(
DestroyPopularityMaybe
::
class
);
}
public
function
test_popularity_calculation_is_queued
()
{
Queue
::
fake
();
// Synchronize package data...
// Assert a job was pushed to any queue
Queue
::
assertPushed
(
CalculatePopularity
::
class
,
function
(
$job
)
use
(
$package
)
{
return
$job
->
package
->
id
===
$package
->
id
;
});
// Assert a job was pushed to a given queue named "popularity"
Queue
::
assertPushedOn
(
'popularity'
,
CalculatePopularity
::
class
);
// Assert a job was pushed twice
Queue
::
assertPushed
(
CalculatePopularity
::
class
,
2
);
// Assert a job was not pushed
Queue
::
assertNotPushed
(
DestroyPopularityMaybe
::
class
);
}
The Mail
facade, when faked, offers four methods: assertSent()
, assertNotSent()
, assertQueued()
, and assertNotQueued()
. Use the Queued
methods when your mail is queued and the Sent
methods when it’s not.
Just like with assertDispatched()
, the first parameter will be the name of the mailable and the second parameter can be empty, the number of times the mailable has been sent, or a closure testing that the mailable has the right data in it. Take a look at Example 12-17 to see a few of these methods in action.
public
function
test_package_authors_receive_launch_emails
()
{
::
fake
();
// Make a package public for the first time...
// Assert a message was sent to a given email address
::
assertSent
(
PackageLaunched
::
class
,
function
(
)
use
(
$package
)
{
return
->
package
->
id
===
$package
->
id
;
});
// Assert a message was sent to given email addresses
::
assertSent
(
PackageLaunched
::
class
,
function
(
)
use
(
$package
)
{
return
->
hasTo
(
$package
->
author
->
)
&&
->
hasCc
(
$package
->
collaborators
)
&&
->
hasBcc
(
'[email protected]'
);
});
// Or, launch two packages...
// Assert a mailable was sent twice
::
assertSent
(
PackageLaunched
::
class
,
2
);
// Assert a mailable was not sent
::
assertNotSent
(
PackageLaunchFailed
::
class
);
}
All of the messages checking for recipients (hasTo()
, hasCc()
, and hasBcc()
) can take either a single email address or an array or collection of addresses.
The Notification
facade, when faked, offers two methods: assertSentTo()
and assertNothingSent()
.
Unlike with the Mail
facade, you’re not going to test who the notification was sent to manually in a closure. Rather, the assertion itself requires the first parameter be either a single notifiable object or an array or collection of them. Only after you’ve passed in the desired notification target can you test anything about the notification itself.
The second parameter is the class name for the notification, and the (optional) third parameter can be a closure defining more expectations about the notification. Take a look at Example 12-18 to learn more.
public
function
test_users_are_notified_of_new_package_ratings
()
{
Notification
::
fake
();
// Perform package rating...
// Assert author was notified
Notification
::
assertSentTo
(
$package
->
author
,
PackageRatingReceived
::
class
,
function
(
$notification
,
$channels
)
use
(
$package
)
{
return
$notification
->
package
->
id
===
$package
->
id
;
}
);
// Assert a notification was sent to the given users
Notification
::
assertSentTo
(
[
$package
->
collaborators
],
PackageRatingReceived
::
class
);
// Or, perform a duplicate package rating...
// Assert a notification was not sent
Notification
::
assertNotSentTo
(
[
$package
->
author
],
PackageRatingReceived
::
class
);
}
You may also find yourself wanting to assert that your channel selection is working—that notifications are sent via the right channels. You can test that as well, as you can see in Example 12-19.
public
function
test_users_are_notified_by_their_preferred_channel
()
{
Notification
::
fake
();
$user
=
factory
(
User
::
class
)
->
create
([
'slack_preferred'
=>
true
]);
// Perform package rating...
// Assert author was notified via Slack
Notification
::
assertSentTo
(
$user
,
PackageRatingReceived
::
class
,
function
(
$notification
,
$channels
)
use
(
$package
)
{
return
$notification
->
package
->
id
===
$package
->
id
&&
in_array
(
'slack'
,
$channels
);
}
);
Testing files can be extraordinarily complex. Many traditional methods require you to actually move files around in your test directories, and formatting the form input and output can be very complicated.
Thankfully, if you use Laravel’s Storage
facade, it’s infinitely simpler to test file uploads and other storage-related items. Example 12-20 demonstrates.
public
function
test_package_screenshot_upload
()
{
Storage
::
fake
(
'screenshots'
);
// Upload a fake image
$response
=
$this
->
postJson
(
'screenshots'
,
[
'screenshot'
=>
UploadedFile
::
fake
()
->
image
(
'screenshot.jpg'
),
]);
// Assert the file was stored
Storage
::
disk
(
'screenshots'
)
->
assertExists
(
'screenshot.jpg'
);
// Or, assert a file does not exist
Storage
::
disk
(
'screenshots'
)
->
assertMissing
(
'missing.jpg'
);
}
Mocks (and their brethren, spies and stubs and dummies and fakes and any number of other tools) are common in testing. We saw some examples of fakes in the previous section. I won’t go into too much detail here, but it’s unlikely you can thoroughly test an application of any size without mocking at least one thing or another.
So, lets take a quick look at mocking in Laravel and how to use Mockery, the mocking library.
Essentially, mocks and other similar tools make it possible to create an object that in some way mimics a real class, but for testing purposes isn’t the real class. Sometimes this is done because the real class is too difficult to instantiate just to inject it into a test, or maybe because the real class communicates with an external service.
As you can probably tell from the examples that follow, Laravel encourages working with the real application as much as possible—which means avoiding too great of a dependence on mocks. But they have their place, which is why Laravel includes Mockery, a mocking library, out of the box, and is why many of its core services offer faking utilities.
Mockery allows you to quickly and easily create mocks from any PHP class in your application. Imagine you have a class that depends on a Slack client, but you don’t want the calls to actually go out to Slack. Mockery makes it simple to create a fake Slack client to use in your tests, like you can see in Example 12-21.
// app/SlackClient.php
class
SlackClient
{
// ...
public
function
send
(
$message
,
$channel
)
{
// Actually sends a message to Slack
}
}
// app/Notifier.php
class
Notifier
{
private
$slack
;
public
function
__construct
(
SlackClient
$slack
)
{
$this
->
slack
=
$slack
;
}
public
function
notifyAdmins
(
$message
)
{
$this
->
slack
->
send
(
$message
,
'admins'
);
}
}
// tests/Unit/NotifierTest.php
public
function
test_notifier_notifies_admins
()
{
$slackMock
=
Mockery
::
mock
(
SlackClient
::
class
)
->
shouldIgnoreMissing
();
$notifier
=
new
Notifier
(
$slackMock
);
$notifier
->
notifyAdmins
(
'Test message'
);
}
There are a lot of elements at work here, but if you look at them one by one, they make sense. We have a class named Notifier
that we’re testing. It has a dependency named SlackClient
that does something that we don’t want it to do when we’re running our tests: it sends actual Slack notifications. So we’re going to mock it.
We use Mockery to get a mock of our SlackClient
class. If we don’t care about what happens to that class—if it should simply exist to keep our tests from throwing errors—we can just use shouldIgnoreMissing()
:
$slackMock
=
Mockery
::
mock
(
SlackClient
::
class
)
->
shouldIgnoreMissing
();
No matter what Notifier
calls on $slackMock
, it’ll just accept it and return null
.
But take a look at test_notifier_notifies_admins()
. At this point, it doesn’t actually test anything.
We could just keep shouldIgnoreMissing()
and then write some assertions below it. That’s usually what we do with shouldIgnoreMissing()
, which makes this object a “fake” or a “stub.”
But what if we want to actually assert that a call was made to the send()
method of SlackClient
? That’s when we drop shouldIgnoreMissing()
and reach for the other should*
methods (Example 12-22).
public
function
test_notifier_notifies_admins
()
{
$slackMock
=
Mockery
::
mock
(
SlackClient
::
class
);
$slackMock
->
shouldReceive
(
'send'
)
->
once
();
$notifier
=
new
Notifier
(
$slackMock
);
$notifier
->
notifyAdmins
(
'Test message'
);
}
shouldReceive('send')->once()
is the same as saying “assert that $slackMock
will have its send()
method called once and only once.” So, we’re now asserting that Notifier
, when we call notifyAdmins()
, makes a single call to the send()
method on SlackClient
.
We could also use something like shouldReceive('send')->times(3)
or shouldReceive('send')->never()
. We can define what parameter we expect to be passed along with that send()
call using with()
, and we can define what to return with andReturn()
:
$slackMock
->
shouldReceive
(
'send'
)
->
with
(
'Hello, world!'
)
->
andReturn
(
true
);
What if we wanted to use the IoC container to resolve our instance of the Notifier
? This might be useful if Notifier
had several other dependencies that we didn’t need to mock.
We can do that! We just use the instance()
method on the container, as in Example 12-23, to tell Laravel to provide an instance of our mock to any classes that request it (which, in this example, will be Notifier
).
public
function
test_notifier_notifies_admins
()
{
$slackMock
=
Mockery
::
mock
(
SlackClient
::
class
);
$slackMock
->
shouldReceive
(
'send'
)
->
once
();
app
()
->
instance
(
SlackClient
::
class
,
$slackMock
);
$notifier
=
app
(
Notifier
::
class
);
$notifier
->
notifyAdmins
(
'Test message'
);
}
In Laravel 5.8+, there’s also a convenient shortcut to creating and binding a Mockery instance to the container:
$this
->
mock
(
SlackClient
::
class
,
function
(
$mock
)
{
$mock
->
shouldReceive
(
'send'
)
->
once
();
});
There’s a lot more you can do with Mockery: you can use spies, and partial spies, and much more. Going deeper into how to use Mockery is out of the scope of this book, but I encourage you to learn more about the library and how it works by reading the Mockery docs.
There’s one other clever thing you can do with Mockery: you can use Mockery methods (e.g., shouldReceive()
) on any facades in your app.
Imagine we have a controller method that uses a facade that’s not one of the fakeable systems we’ve already covered; we want to test that controller method and assert that a certain facade call was made.
Thankfully, it’s simple: we can run our Mockery-style methods on the facade, as you can see in Example 12-25.
// PersonController
public
function
index
()
{
return
Cache
::
remember
(
'people'
,
function
()
{
return
Person
::
all
();
});
}
// PeopleTest
public
function
test_all_people_route_should_be_cached
()
{
$person
=
factory
(
Person
::
class
)
->
create
();
Cache
::
shouldReceive
(
'remember'
)
->
once
()
->
andReturn
(
collect
([
$person
]));
$this
->
get
(
'people'
)
->
assertJsonFragment
([
'name'
=>
$person
->
name
]);
}
As you can see, you can use methods like shouldReceive()
on the facades, just like you do on a Mockery
object.
You can also use your facades as spies, which means you can set your assertions at the end and use shouldHaveReceived()
instead of should
Receive()
. Example 12-26 illustrates this.
public
function
test_package_should_be_cached_after_visit
()
{
Cache
::
spy
();
$package
=
factory
(
Package
::
class
)
->
create
();
$this
->
get
(
route
(
'packages.show'
,
[
$package
->
id
]));
Cache
::
shouldHaveReceived
(
'put'
)
->
once
()
->
with
(
'packages.'
.
$package
->
id
,
$package
->
toArray
());
}
We’ve covered a lot in this chapter, but we’re almost done! We have just two more pieces of Laravel’s testing arsenal to cover: Artisan and the browser.
If you’re working in Laravel prior to 5.7, the best way to test Artisan commands is to call them with $this->artisan($commandName, $parameters)
and then test their impact, like in Example 12-27.
public
function
test_promote_console_command_promotes_user
()
{
$user
=
factory
(
User
::
class
)
->
create
();
$this
->
artisan
(
'user:promote'
,
[
'userId'
=>
$user
->
id
]);
$this
->
assertTrue
(
$user
->
isPromoted
());
}
You can also make assertions against the response code you get from Artisan, as you can see in Example 12-28.
$code
=
$this
->
artisan
(
'do:thing'
,
[
'--flagOfSomeSort'
=>
true
]);
$this
->
assertEquals
(
0
,
$code
);
// 0 means "no errors were returned"
If you’re working with Laravel 5.7 and later, you can also chain three new methods onto your $this->artisan()
call: expectsQuestion()
, expectsOutput()
, and assert
ExitCode()
. The expects
* methods will work on any of the interactive prompts, including confirm()
, and anticipate()
, and the assertExitCode()
method is a shortcut to what we saw in Example 12-28.
Take a look at Example 12-29 to see how it works.
// routes/console.php
Artisan
::
command
(
'make:post {--expanded}'
,
function
()
{
$title
=
$this
->
ask
(
'What is the post title?'
);
$this
->
comment
(
'Creating at '
.
Str
::
slug
(
$title
)
.
'.md'
);
$category
=
$this
->
choice
(
'What category?'
,
[
'technology'
,
'construction'
],
0
);
// Create post here
$this
->
comment
(
'Post created'
);
});
// Test file
public
function
test_make_post_console_commands_performs_as_expected
()
{
$this
->
artisan
(
'make:post'
,
[
'--expanded'
=>
true
])
->
expectsQuestion
(
'What is the post title?'
,
'My Best Post Now'
)
->
expectsOutput
(
'Creating at my-best-post-now.md'
)
->
expectsQuestion
(
'What category?'
,
'construction'
)
->
expectsOutput
(
'Post created'
)
->
assertExitCode
(
0
);
}
As you can see, the first parameter of expectsQuestion()
is the text we’re expecting to see from the question, and the second parameter is the text we’re answering with. expectsOutput()
just tests that the passed string is returned.
We’ve made it to browser tests! These allow you to actually interact with the DOM of your pages: in browser tests you can click buttons, fill out and submit forms, and, with Dusk, even interact with JavaScript.
Laravel actually has two separate browser testing tools: BrowserKit Testing and Dusk. Only Dusk is actively maintained; BrowserKit Testing seems to have become a bit of a second-class citizen, but it’s still available on GitHub and still works at the time of this writing.
For browser testing, I suggest you use the core application testing tools whenever possible (those we’ve covered up to this point). If your app is not JavaScript-based and you need to test actual DOM manipulation or form UI elements, use BrowserKit. If you’re developing a JavaScript-heavy app, you’ll likely want to use Dusk, which we’ll cover next.
However, there will also be many instances where you’ll want to use a JavaScript-based test stack (which is out of scope for this book) based on something like Jest and vue-test-utils
. This toolset can be very useful for Vue component testing, and Jest’s snapshot functionality simplifies the process of keeping API and frontend test data in sync. To learn more, check out Caleb Porzio’s “Getting Started” blog post and Samantha Geitz’s 2018 Laracon talk.
If you’re working with a JavaScript framework other than Vue, there are no currently preferred frontend testing solutions in the Laravel world. However, the broad React world seems to have settled on Jest and Enzyme.
Dusk is a Laravel tool (installable as a Composer package) that makes it easy to write Selenium-style directions for a ChromeDriver-based browser to interact with your app. Unlike most other Selenium-based tools, Dusk’s API is simple and it’s easy to write code to interact with it by hand. Take a look:
$this
->
browse
(
function
(
$browser
)
{
$browser
->
visit
(
'/register'
)
->
type
(
'email'
,
'[email protected]'
)
->
type
(
'password'
,
'secret'
)
->
press
(
'Sign Up'
)
->
assertPathIs
(
'/dashboard'
);
});
With Dusk, there’s an actual browser spinning up your entire application and interacting with it. That means you can have complex interactions with your JavaScript and get screenshots of failure states—but it also means everything’s a bit slower and it’s more prone to failure than Laravel’s base application testing suite.
Personally, I’ve found that Dusk is most useful as a regression testing suite, and it works better than something like Selenium. Rather than using it for any sort of test-driven development, I use it to assert that the user experience hasn’t broken (“regressed”) as the app continues to develop. Think of this more like writing tests about your user interface after the interface is built.
The Dusk docs are robust, so I’m not going to go into great depth here, but I want to show you the basics of working with Dusk.
To install Dusk, run these two commands:
composer require --dev laravel/dusk php artisan dusk:install
Then edit your .env file to set your APP_URL
variable to the same URL you use to view your site in your local browser; something like http://mysite.test
.
To run your Dusk tests, just run php artisan dusk
. You can pass in all the same parameters you’re used to from PHPUnit (for example, php artisan dusk --filter=my_best_test
).
To generate a new Dusk test, use a command like the following:
php artisan dusk:make RatingTest
This test will be placed in tests/Browser/RatingTest.php.
You can customize the environment variables for Dusk by creating a new file named .env.dusk.local (and you can replace .local if you’re working in a different environment, like “staging”).
To write your Dusk tests, imagine that you’re directing one or more web browsers to visit your application and take certain actions. That’s what the syntax will look like, as you can see in Example 12-30.
public
function
testBasicExample
()
{
$user
=
factory
(
User
::
class
)
->
create
();
$this
->
browse
(
function
(
$browser
)
use
(
$user
)
{
$browser
->
visit
(
'login'
)
->
type
(
'email'
,
$user
->
)
->
type
(
'password'
,
'secret'
)
->
press
(
'Login'
)
->
assertPathIs
(
'/home'
);
});
}
$this->browse()
creates a browser, which you pass into a closure; then, within the closure you instruct the browser which actions to take.
It’s important to note that—unlike Laravel’s other application testing tools, which mimic the behavior of your forms—Dusk is actually spinning up a browser, sending events to the browser to type those words, and then sending an event to the browser to press that button. This is a real browser and Dusk is fully driving it.
You can also “ask” for more than one browser by adding parameters to the closure, which allows you to test how multiple users might interact with the website (for example, with a chat system). Take a look at Example 12-31, from the docs.
$this
->
browse
(
function
(
$first
,
$second
)
{
$first
->
loginAs
(
User
::
find
(
1
))
->
visit
(
'home'
)
->
waitForText
(
'Message'
);
$second
->
loginAs
(
User
::
find
(
2
))
->
visit
(
'home'
)
->
waitForText
(
'Message'
)
->
type
(
'message'
,
'Hey Taylor'
)
->
press
(
'Send'
);
$first
->
waitForText
(
'Hey Taylor'
)
->
assertSee
(
'Jeffrey Way'
);
});
There’s a huge suite of actions and assertions available that we won’t cover here (check the docs), but let’s look at a few of the other tools Dusk provides.
As you can see in Example 12-31, the syntax for authentication is a little different from the rest of the Laravel application testing: $browser->loginAs($user)
.
Don’t use the RefreshDatabase
trait with Dusk! Use the DatabaseMigrations
trait instead; transactions, which RefreshDatabase
uses, don’t last across requests.
If you’ve ever written jQuery, interacting with the page using Dusk will come naturally. Take a look at Example 12-32 to see the common patterns for selecting items with Dusk.
<--
Template
--
>
<div
class=
"search"
><input><button
id=
"search-button"
></button></div>
<button
dusk=
"expand-nav"
></button>
// Dusk tests
// Option 1: jQuery-style syntax
$browser
->
click
(
'.search button'
);
$browser
->
click
(
'#search-button'
);
// Option 2: dusk="selector-here" syntax; recommended
$browser
->
click
(
'@expand-nav'
);
As you can see, adding the dusk
attribute to your page elements allows you to reference them directly in a way that won’t change when the display or layout of the page changes later; when any method asks for a selector, pass in the @
sign and then the content of your dusk
attribute.
Let’s take a look at a few of the methods you can call on $browser
.
To work with text and attribute values, use these methods:
value($selector, $value = null)
Returns the value of any text input if only one parameter is passed; sets the value of an input if a second parameter is passed.
text($selector)
Gets the text content of a nonfillable item like a <div>
or a <span>
.
attribute($selector, $attributeName)
Returns the value of a particular attribute on the element matching $selector
.
Methods for working with forms and files include the following:
type($selector, $valueToType)
Similar to value()
, but actually types the characters rather than directly setting the value.
With methods like type()
that target inputs, Dusk will start by trying to match a Dusk or CSS selector, and then will look for an input with the provided name, and finally will try to find a <textarea>
with the provided name.
select($selector, $optionValue)
Selects the option with the value of $optionValue
in a drop-down selectable by $selector
.
check($selector)
and uncheck($selector)
radio($selector, $optionValue)
Selects the option with the value of $optionValue
in a radio group selectable by $selector
.
attach($selector, $filePath)
Attaches a file at $filePath
to the file input selectable by $selector
.
The methods for keyboard and mouse input are:
clickLink($selector)
click($selector)
and mouseover($selector)
drag($selectorToDrag, $selectorToDragTo)
dragLeft()
, dragRight()
, dragUp()
, dragDown()
Given a first parameter of a selector and a second parameter of a number of pixels, drags the selected item that many pixels in the given direction.
keys($selector, $instructions)
Sends keypress events within the context of $selector
according to the instructions in $instructions
. You can even combine modifiers with your typing:
$browser
->
keys
(
'selector'
,
'this is '
,
[
'{shift}'
,
'great'
]);
This would type “this is GREAT”. As you can see, adding an array to the list of items to type allows you to combine modifiers (wrapped with {}
) with typing. You can see a full list of the possible modifiers in the Facebook WebDriver source.
If you’d like to just send your key sequence to the page (for example, to trigger a keyboard shortcut), you can target the top level of your app or page as your selector. For example, if it’s a Vue app and the top level is a <div>
with an ID of app
:
$browser
->
keys
(
'#app'
,
[
'{command}'
,
'/'
]);
Because Dusk interacts with JavaScript and is directing an actual browser, the concept of time and timeouts and “waiting” needs to be addressed. Dusk offers a few methods you can use to ensure your tests handle timing issues correctly. Some of these methods are useful for interacting with intentionally slow or delayed elements of the page, but some of them are also just useful for getting around initialization times on your components. The available methods include the following:
pause($milliseconds)
Pauses the execution of Dusk tests for the given number of milliseconds. This is the simplest “wait” option; it makes any future commands you send to the browser wait that amount of time before operating.
You can use this and other waiting methods in the midst of an assertion chain, as shown here:
$browser
->
click
(
'chat'
)
->
pause
(
500
)
->
assertSee
(
'How can we help?'
);
waitFor($selector, $maxSeconds = null)
and waitForMissing($selector,
$maxSeconds
= null)
Waits until the given element exists on the page (waitFor()
) or disappears from the page (waitForMissing()
) or times out after the optional second parameter’s second count:
$browser
->
waitFor
(
'@chat'
,
5
);
$browser
->
waitUntilMissing
(
'@loading'
,
5
);
whenAvailable($selector, $callback)
Similar to waitFor()
, but accepts a closure as the second parameter which will define what action to take when the specified element becomes available:
$browser
->
whenAvailable
(
'@chat'
,
function
(
$chat
)
{
$chat
->
assertSee
(
'How can we help you?'
);
});
waitForText($text, $maxSeconds = null)
Waits for text to show up on the page, or times out after the optional second parameter’s second count:
$browser
->
waitForText
(
'Your purchase has been completed.'
,
5
);
waitForLink($linkText, $maxSeconds = null)
Waits for a link to exist with the given link text, or times out after the optional second parameter’s second count:
$browser
->
waitForLink
(
'Clear these results'
,
2
);
waitForLocation($path)
Waits until the page URL matches the provided path:
$browser
->
waitForLocation
(
'auth/login'
);
waitForRoute($routeName)
Waits until the page URL matches the URL for the provided route:
$browser
->
waitForRoute
(
'packages.show'
,
[
$package
->
id
]);
waitForReload()
waitUntil($expression)
Waits until the provided JavaScript expression evaluates as true:
$browser
->
waitUntil
(
'App.packages.length > 0'
,
7
);
As I’ve mentioned, there’s a huge list of assertions you can make against your app with Dusk. Here are a few that I use most commonly—you can see the full list in the Dusk docs:
assertTitleContains($text)
assertQueryStringHas($keyName)
assertHasCookie($cookieName)
assertSourceHas($htmlSourceCode)
assertChecked($selector)
assertSelectHasOption($selectorForSelect, $optionValue)
assertVisible($selector)
assertFocused()
assertVue($dataLocation, $dataValue, $selector)
So far, everything we’ve covered makes it possible to test individual elements on our pages. But we’ll often use Dusk to test more complex applications and single-page apps, which means we’re going to need organizational structures around our assertions.
The first organizational structures we have encountered have been the dusk
attribute (e.g., <div dusk="abc">
, creating a selector named @abc
we can refer to later) and the closures we can use to wrap certain portions of our code (e.g., with when
Available()
).
Dusk offers two more organizational tools: pages and components. Let’s start with pages.
A page is a class that you’ll generate which contains two pieces of functionality: first, a URL and assertions to define which page in your app should be attached to this Dusk page; and second, shorthand like we used inline (the @abc
selector generated by the dusk="abc"
attribute in our HTML) but just for this page, and without needing to edit our HTML.
Let’s imagine our app has a “create package” page. We can generate a Dusk page for it as follows:
php
artisan
dusk
:
page
CreatePackage
Take a look at Example 12-33 to see what our generated class will look like.
<?
php
namespace
TestsBrowserPages
;
use
LaravelDuskBrowser
;
class
CreatePackage
extends
Page
{
/**
* Get the URL for the page
*
* @return string
*/
public
function
url
()
{
return
'/'
;
}
/**
* Assert that the browser is on the page
*
* @param Browser $browser
* @return void
*/
public
function
assert
(
Browser
$browser
)
{
$browser
->
assertPathIs
(
$this
->
url
());
}
/**
* Get the element shortcuts for the page
*
* @return array
*/
public
function
elements
()
{
return
[
'@element'
=>
'#selector'
,
];
}
}
The url()
method defines the location where Dusk should expect this page to be; assert()
lets you run additional assertions to verify you’re on the right page, and elements()
provides shortcuts for @dusk
-style selectors.
Let’s make a few quick modifications to our “create package” page, to make it look like Example 12-34.
class
CreatePackage
extends
Page
{
public
function
url
()
{
return
'/packages/create'
;
}
public
function
assert
(
Browser
$browser
)
{
$browser
->
assertTitleContains
(
'Create Package'
);
$browser
->
assertPathIs
(
$this
->
url
());
}
public
function
elements
()
{
return
[
'@title'
=>
'input[name=title]'
,
'@instructions'
=>
'textarea[name=instructions]'
,
];
}
}
Now that we have a functional page, we can navigate to it and access its defined elements:
// In a test
$browser
->
visit
(
new
TestsBrowserPagesCreatePackage
)
->
type
(
'@title'
,
'My package title'
);
One common use for pages is to define a common action you want to take in your tests; consider these almost like macros for Dusk. You can define a method on your page and then call it from your code, as you can see in Example 12-35.
class
CreatePackage
extends
Page
{
// ... url(), assert(), elements()
public
function
fillBasicFields
(
Browser
$browser
,
$packageTitle
=
'Best package'
)
{
$browser
->
type
(
'@title'
,
$packageTitle
)
->
type
(
'@instructions'
,
'Do this stuff and then that stuff'
);
}
}
$browser
->
visit
(
new
CreatePackage
)
->
fillBasicFields
(
'Greatest Package Ever'
)
->
press
(
'Create Package'
)
->
assertSee
(
'Greatest Package Ever'
);
If you want the same functionality as Dusk pages offer, but without it being constrained to a specific URL, you’ll likely want to reach for Dusk components. These classes are shaped very similarly to pages, but instead of being bound to a URL, they’re each bound to a selector.
In NovaPackages.com, we have a little Vue component for rating packages and displaying ratings. Let’s make a Dusk component for it:
php artisan dusk:component RatingWidget
Take a look at Example 12-36 to see what that will generate.
<?
php
namespace
TestsBrowserComponents
;
use
LaravelDuskBrowser
;
use
LaravelDuskComponent
as
BaseComponent
;
class
RatingWidget
extends
BaseComponent
{
/**
* Get the root selector for the component
*
* @return string
*/
public
function
selector
()
{
return
'#selector'
;
}
/**
* Assert that the browser page contains the component
*
* @param Browser $browser
* @return void
*/
public
function
assert
(
Browser
$browser
)
{
$browser
->
assertVisible
(
$this
->
selector
());
}
/**
* Get the element shortcuts for the component
*
* @return array
*/
public
function
elements
()
{
return
[
'@element'
=>
'#selector'
,
];
}
}
As you can see, this is basically the same as a Dusk page, but we’re encapsulating our work to an HTML element instead of a URL. Everything else is basically the same. Take a look at Example 12-37 to see our rating widget example in Dusk component form.
class
RatingWidget
extends
BaseComponent
{
public
function
selector
()
{
return
'.rating-widget'
;
}
public
function
assert
(
Browser
$browser
)
{
$browser
->
assertVisible
(
$this
->
selector
());
}
public
function
elements
()
{
return
[
'@5-star'
=>
'.five-star-rating'
,
'@4-star'
=>
'.four-star-rating'
,
'@3-star'
=>
'.three-star-rating'
,
'@2-star'
=>
'.two-star-rating'
,
'@1-star'
=>
'.one-star-rating'
,
'@average'
=>
'.average-rating'
,
'@mine'
=>
'.current-user-rating'
,
];
}
public
function
ratePackage
(
Browser
$browser
,
$rating
)
{
$browser
->
click
(
"@
{
$rating
}
-star"
)
->
assertSeeIn
(
'@mine'
,
$rating
);
}
}
Using components works just like using pages, as you can see in Example 12-38.
$browser
->
visit
(
'/packages/tightenco/nova-stock-picker'
)
->
within
(
new
RatingWidget
,
function
(
$browser
)
{
$browser
->
ratePackage
(
2
);
$browser
->
assertSeeIn
(
'@average'
,
2
);
});
That’s a good, brief overview of what Dusk can do. There’s a lot more—more assertions, more edge cases, more gotchas, more examples—in the Dusk docs, so I’d recommend a read through there if you plan to work with Dusk.
Laravel can work with any modern PHP testing framework, but it’s optimized for PHPUnit (especially if your tests extend Laravel’s TestCase
). Laravel’s application testing framework makes it simple to send fake HTTP and console requests through your application and inspect the results.
Tests in Laravel can easily and powerfully interact with and assert against the database, cache, session, filesystem, mail, and many other systems. Quite a few of these systems have fakes built in to make them even easier to test. You can test DOM and browser-like interactions with BrowserKit Testing or Dusk.
Laravel brings in Mockery in case you need mocks, stubs, spies, dummies, or anything else, but the testing philosophy of Laravel is to use real collaborators as much as possible. Don’t fake it unless you have to.