Chapter 12. Testing

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.

Testing Basics

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.

Differences in Testing Prior to Laravel 5.4

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:

  1. 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.

  2. 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().

  3. 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.

  4. 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.

The Basics of PHPUnit Assertions

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.

Example 12-1. tests/Feature/ExampleTest.php
<?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.

Example 12-2. Sample ExampleTest output
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.

Example 12-3. tests/Feature/FailingTest.php, edited to fail
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.

Generating Unit Tests

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.

Example 12-4. Sample failing test output
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.

Naming Tests

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.

Example 12-5. Naming PHPUnit methods
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
    }
}

The Testing Environment

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"/>.

The Testing Traits

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.

RefreshDatabase

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.

WithoutMiddleware

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.

DatabaseMigrations

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.

DatabaseTransactions

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.

Simple Unit Tests

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.

Example 12-6. A simple unit test
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.

Example 12-7. A more complicated unit test
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.

Application Testing: How It Works

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?

TestCase

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.

HTTP Tests

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.

Testing Basic Pages with $this->get() and Other HTTP Calls

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.

Example 12-8. A simple use of post() in testing
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!

Testing JSON APIs with $this->getJson() and Other JSON HTTP Calls

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.

Example 12-9. A simple use of postJSON() in testing
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();
}

Assertions Against $response

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.

Authenticating Responses

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.

Example 12-10. Basic auth in testing
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();
}

Using Factory States for Authorization

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.

A Few Other Customizations to Your HTTP Tests

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');

Handling Exceptions in Application Tests

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.

Example 12-11. An exception that will be captured by Laravel’s exception handler and result in a passing 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.

Example 12-12. Temporarily disabling exception handling in a single test
// 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().

Debugging Responses

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();

Database Tests

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.

Example 12-13. Sample database tests
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.

Using Model Factories in Tests

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.

Seeding in Tests

If you use seeds in your application, you can run the equivalent of php artisan db:seed by running $this->seed() in your test.

You can also pass a seeder class name to just seed that one class:

$this->seed(); // Seeds all
$this->seed(UserSeeder::class); // Seeds users

Testing Other Laravel Systems

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.

Event Fakes

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.

Example 12-14. Suppressing events without adding assertions
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.

Example 12-15. Making assertions against events
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.

Event::fake() Disables Eloquent Model Events

Event::fake() also disables Eloquent model events. So if you have any important code, for example, in a model’s creating event, make sure to create your models (through your factories or however else) before calling Event::fake().

Bus and Queue Fakes

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.

Example 12-16. Faking jobs and queued jobs
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);
}

Mail Fakes

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.

Example 12-17. Making assertions against mail
public function test_package_authors_receive_launch_emails()
{
    Mail::fake();

    // Make a package public for the first time...

    // Assert a message was sent to a given email address
    Mail::assertSent(PackageLaunched::class, function ($mail) use ($package) {
        return $mail->package->id === $package->id;
    });

    // Assert a message was sent to given email addresses
    Mail::assertSent(PackageLaunched::class, function ($mail) use ($package) {
        return $mail->hasTo($package->author->email) &&
               $mail->hasCc($package->collaborators) &&
               $mail->hasBcc('[email protected]');
    });

    // Or, launch two packages...

    // Assert a mailable was sent twice
    Mail::assertSent(PackageLaunched::class, 2);

    // Assert a mailable was not sent
    Mail::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.

Notification Fakes

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.

Example 12-18. Notification fakes
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.

Example 12-19. Testing notification channels
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);
        }
    );

Storage Fakes

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.

Example 12-20. Testing storage and file uploads with storage fakes
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');
}

Mocking

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.

A Quick Introduction to Mocking

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.

A Quick Introduction to Mockery

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.

Example 12-21. Using Mockery in Laravel
// 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).

Example 12-22. Using the shouldReceive() method on a Mockery mock
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).

Example 12-23. Binding a Mockery instance to the container
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:

Example 12-24. Binding Mockery instances to the container more easily in Laravel 5.8+
$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.

Faking Other Facades

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.

Example 12-25. Mocking a facade
// 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 shouldReceive(). Example 12-26 illustrates this.

Example 12-26. Facade spies
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());
}

Testing Artisan Commands

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.

Example 12-27. Simple Artisan tests
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.

Example 12-28. Manually asserting Artisan exit codes
$code = $this->artisan('do:thing', ['--flagOfSomeSort' => true]);
$this->assertEquals(0, $code); // 0 means "no errors were returned"

Asserting Against Artisan Command Syntax

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 assertExitCode(). 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.

Example 12-29. Basic Artisan “expects” tests
// 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.

Browser Tests

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.

Choosing a Tool

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.

Testing with Dusk

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.

Installing 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).

Writing Dusk tests

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.

Customizing Dusk Environment Variables

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.

Example 12-30. A simple Dusk test
public function testBasicExample()
{
    $user = factory(User::class)->create();

    $this->browse(function ($browser) use ($user) {
        $browser->visit('login')
            ->type('email', $user->email)
            ->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.

Example 12-31. Multiple Dusk browsers
$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.

Authentication and databases

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).

Avoid the RefreshDatabase Trait with Dusk

Don’t use the RefreshDatabase trait with Dusk! Use the DatabaseMigrations trait instead; transactions, which RefreshDatabase uses, don’t last across requests.

Interactions with the page

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.

Example 12-32. 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.

Dusk’s Selector Matching Order

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)

Checks or unchecks a checkbox selectable by $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)

Follows a text link to its target.

click($selector) and mouseover($selector)

Triggers a mouse click or a mouseover event on $selector.

drag($selectorToDrag, $selectorToDragTo)

Drags an item to another item.

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}', '/']);

Waiting

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()

Waits until the page reloads.

waitUntil($expression)

Waits until the provided JavaScript expression evaluates as true:

$browser->waitUntil('App.packages.length > 0', 7);

Other assertions

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)

Other organizational structures

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 whenAvailable()).

Dusk offers two more organizational tools: pages and components. Let’s start with pages.

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.

Example 12-33. The generated Dusk page
<?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.

Example 12-34. A simple “create package” Dusk page
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.

Example 12-35. Defining and using a custom page method
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');

Components

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.

Example 12-36. The default source of a generated Dusk component
<?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.

Example 12-37. A Dusk component for the rating widget
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.

Example 12-38. Using Dusk components
$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.

TL;DR

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.117.109