Browser, Feature and Unit Testing in Laravel 5.7

Browser, Feature and Unit Testing in Laravel 5.

7Audun RundbergBlockedUnblockFollowFollowingJan 17It 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 testsFirst, let’s look at an example of a browser test.

This is the file tests/Browser/ExampleCompanyTest.

php 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 TestsBrowser;use AppUser;use LaravelDuskBrowser;use TestsDuskTestCase;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 php artisan duskWhen 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 tests/Browser/screenshotsHere’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)$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 setUp function), then visit /companies/create, then wait for the company-form to appear, then type the name, organization_number and so on into the form before we click on the button save-company.

->waitFor('@companies-table') ->assertPathIs('/companies') ->assertSee('Example company');Then we wait for the companies-table to appear, and assert that the path is no longer /companies/create, but /companies, and we assert that the name of the company can be seen on the page.

If you are wondering what the @ symbol and @company-form 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 dusk attribute to an element like this: <form dusk="company-form">.

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 testsSo 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 tests/Feature/ExampleCompanyTest.

php.

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 TestsFeature;use AppCompany;use AppUser;use IlluminateFoundationTestingRefreshDatabase;use TestsTestCase;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 vendor/bin/phpunit.

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 $user, make a $company and add the $company to the $user.

Then we create $anotherUser.

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 $anotherUser, we try to get the company that belongs to $user, 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 testsThis 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 tests/Unit/ExampleProductTest.

php.

The file contains two tests.

The tests check that the Product model returns correct values for the attribute was_bought_with_tax<?phpnamespace TestsUnit;use AppProduct;use TestsTestCase;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 vendor/bin/phpunit.

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 was_bought_with_tax is true.

Which is a test of this function in the model app/Product.

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

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

.

. More details

Leave a Reply