Browser, Feature and Unit Testing in Laravel 5.7

It wasn’t until I wrote my first browser test (using Laravel Dusk), that I started to understand what the different types of tests mentioned in the Laravel documentation were.

I had already written a bunch of feature tests to test APIs, but didn’t understand how the concept of a “feature test” differed from a “browser test” or a “unit test”.

It finally dawned on me that both browser and feature tests test features of your application, but browser tests test those features in a real web browser. Yes, you’re allowed to think I’m a slow learner.

I’ve since then started writing three different types of tests in my Laravel applications. Here they are, with a typical use case:

  • Browser: Test a series of actions like opening a page and submitting a form in a real web browser, which means you can test Javascript applications.
  • Feature: POST some data to a REST endpoint, and check if the API returns the expected result.
  • Unit: Create a model, and check that the model attributes are as expected.

Browser tests

First, let’s look at an example of a browser test.

This is the file in my project. I removed a bunch of other tests from this file for simplicity.

What we’re testing for here, is if we can use the frontend of the application (written in Vue/JavaScript) to create a company:

<?phpnamespace Tests\Browser;use App\User;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleCompanyTest extends DuskTestCase
{
public function testCreateCompany()
{
$user = User::find(1);
$this->browse(function (Browser $browser) use ($user) {
$browser->loginAs($user)
->visit('/companies/create')
->waitFor('@company-form')
->type('@name', 'Example company')
->type('@organization_number', '123456789')
->type('@bank_account', '1234 5678 9012')
->type('@net_days', '30')
->type('@address', 'Corporate Street 1')
->type('@phone', '+47 900 90 900')
->type('@email', 'hello@examplecompany.com')
->type('@website', 'examplecompany.com')
->click('@save-company')
->waitFor('@companies-table')
->assertPathIs('/companies')
->assertSee('Example company');
});
}
}

The test can be run with the command

When you run this command, Dusk starts an invisible copy of Google Chrome. Dusk then takes control of Chrome and performs the commands in the test just like a real user. The obvious benefit is that once the code is written, you have automated the testing and you can now test this part of your application much faster and much more often.

If the browser test fails, Laravel Dusk will save a screenshot of the failed state to the folder

Here’s a recording of me manually testing this form (it takes me 20-30 seconds to complete):

Let’s get back to the code. The interesting part of the test starts with the line

$browser->loginAs($user)
->visit('/companies/create')
->waitFor('@company-form')
->type('@name', 'Example company')
->type('@organization_number', '123456789')
->type('@bank_account', '1234 5678 9012')
->type('@net_days', '30')
->type('@address', 'Corporate Street 1')
->type('@phone', '+47 900 90 900')
->type('@email', 'hello@examplecompany.com')
->type('@website', 'examplecompany.com')
->click('@save-company')

We login as an existing user (found in the class function), then visit , then wait for the to appear, then type the , and so on into the form before we click on the button .

    ->waitFor('@companies-table')
->assertPathIs('/companies')
->assertSee('Example company');

Then we wait for the to appear, and assert that the path is no longer , but , and we assert that the name of the company can be seen on the page.

If you are wondering what the symbol and means, it’s a way for Laravel Dusk to find elements in your HTML code without using CSS selectors. So in your HTML, you can add the attribute to an element like this: . You can of course, use CSS selectors if you want.

Also, if you’re thinking that this test requires a little more code to work properly every time, you’re right. Once the test has been run, there will already be a company called Example company in the database, so the database really should be reset after the test.

Feature tests

So now that we know that we can use browser tests to automate user behavior in a browser, what can we do with feature tests?

Here’s an example from . The file contains one test. It checks if the API returns 403 Forbidden if a user tries to access a company that the user does not have access to.

<?phpnamespace Tests\Feature;use App\Company;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleCompanyTest extends TestCase
{
use RefreshDatabase;
public function testGetCompanyUnauthorized()
{
$user = factory(User::class)->create();
$company = factory(Company::class)->make();
$user->addCompany($company);
$anotherUser = factory(User::class)->create();
$response = $this
->actingAs($anotherUser, 'api')
->get('/api/v1/companies/'.$company->id);
$response->assertStatus(403);
}
}

The test can be run with the command . This test takes 680 ms to run, while the browser test took 3.43 seconds.

The interesting part of the test starts with creating some models:

$user = factory(User::class)->create();
$company = factory(Company::class)->make();
$user->addCompany($company);
$anotherUser = factory(User::class)->create();

Using model factories, we create a , make a and add the to the . Then we create . So one user has a company, and the other does not.

$response = $this
->actingAs($anotherUser, 'api')
->get('/api/v1/companies/'.$company->id);
$response->assertStatus(403);

Then, acting as , we try to get the company that belongs to , which should be impossible.

We then assert that the status returned is 403 Forbidden.

So although the attempt to get the company fails, the test passes because we’re checking that you can’t access a company that doesn’t belong to you.

Note: The test should probably also check that the data returned contains the correct error message, and not the company data.

Unit tests

This is the last type of the three that I’ve tried to write, and it’s been a real eye-opener to see how you can use unit tests to verify that the business logic of your application.

Here’s an example from . The file contains two tests. The tests check that the model returns correct values for the attribute

<?phpnamespace Tests\Unit;use App\Product;
use Tests\TestCase;
class ExampleProductTest extends TestCase
{
public function testProductWasBoughWithTax()
{
$product = factory(Product::class)->make([
'tax_rate' => 0.06,
]);
$this->assertTrue($product->was_bought_with_tax);
}
public function testProductWasNotBoughWithTax()
{
$product = factory(Product::class)->make([
'tax_rate' => 0,
]);
$this->assertFalse($product->was_bought_with_tax);
}
}

The unit tests can also be run with the command . These two tests take 146ms to run, far less than the feature and browser tests.

If we look at the first test:

$product = factory(Product::class)->make([
'tax_rate' => 0.06,
]);

Using a factory, we make a new product, and set the tax rate to 6%.

$this->assertTrue($product->was_bought_with_tax);

We then assert that the attribute is true.

Which is a test of this function in the model

public function getWasBoughtWithTaxAttribute()
{
return (bool) ($this->tax_rate > 0);
}

The examples I’m using here come from an application that deals with the intricacies of buying and selling vintage furniture and getting the sales tax right, so having tests that check that all calculations are OK has been quite useful.

Summary

If you’re familiar with testing, and especially test-driven development, you might be shaking your head right now.

If you didn’t know this already, when you’re doing test-driven development, you write tests like the ones in my examples first, which means they will fail, and then you write code until the test passes.

99% of the time while writing tests for this application, I’ve written them to check if existing code works. At one point I was testing one of the tax calculations, and noticed that the entire calculation was wrong to begin with (and had been for a few days).

Since I had just written the test, I started to question whether there was something wrong with the test or if the calculation in the model was wrong, which I believe is something that I could have avoided if I had written the test first.

Content advisor at Netlife Design, Oslo

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store