How to Write Feature and Unit Tests for Laravel

by Laura Martin
7 min read

Automated tests are a very important part of software development. We use them to test the oft-used parts of a website, such as content management so that we don't have to test the system in its entirety each time a new feature has been added.

Feature and Unit tests are methods of software testing that isolate a part of a system and test it. A unit test could look at something small like a single method e.g. adding a row to a database, whereas a feature test will have a broader view such as registering a user and validating their details. You write the test with a desired outcome in mind and it doesn't always have to be that XYZ works, you could test that something is meant to fail.

In this guide, I will take you through how to make some simple tests for a Laravel project using a Page editor as an example. You can download all the files required here.

GETTING STARTED

NECESSARY

You will need Laravel installed and running. At time of writing this, I am using Laravel 8.42 running on PHP 8. I also use Valet to serve websites but you could use Docker if you so wish, this is what Laravel currently recommends.

OPTIONAL

None of the following are needed for testing but is included in the example repository as we use them on our builds.

We develop in a Domain Driven Design (DDD) using Boilerplate as a starting point, this puts our code into a specific structure so we have all the Page content in its own domain folder rather than one folder to contain all the models, another for the controllers. I have also used Livewire with Livewire Tables for the list of posts displayed below, and Spatie Sluggable is used to make slugs for the pages, so they have unique URLs.

SET UP THE PAGE DOMAIN

I've made a very simple table to store pages in, with a title, slug (from Spatie Sluggable) and content only. There is an index that uses a Livewire data table component to show a list from the model, and views to create and edit each entry.

Screenshot of livewire table
The pages index with a Livewire table.

WRITING TESTS

There are many, many ways you can test your project. In my example of a simple Page editor, I have written quick tests for all of the below. These are all examples of the features we would not want to test manually as development on the project progresses, instead allowing PHPUnit to tell us when something is wrong.

HTTP TESTS

AUTHORISATION

On our system, we have 2 roles: admins and users. Admins can access the admin panel and have full access to the system, whereas a user can only login to the front-facing area. We can write a test to check that only admins can access any of the Page views.

/* @test */
public function ausercannotaccessthecreatepage_view()
{
    // Login as a user
    $this->actingAs(User::factory()->user()->create());
    // Simulate a GET request to the given URL
    $response = $this->get('/admin/pages/create');
    // Check the response, we should have been
    // redirected to the homepage
    $response->assertRedirect('/');
}

You'll notice that we haven't used routes when testing, but are instead using the URLs. This is as we may have linked to the URLs in content, menus, or anywhere else really. Testing the URLs like this lets us know the route has changed, so we either need to update the URLs where they are used or fix the route if necessary.

VALIDATION

When you set up storing and updating a model you'll use Request classes to handle the validation of the data. This could be as simple or complex as you need it to be. Laravel has a list of built-in rules you can start with, which will cover the majority of cases. On our example repo, we have the title and content fields set as required strings:

public function rules(): array
{
    return [
        'title' => 'string|required',
        'content' => 'string|required',
    ];
}

We can write tests to check validation. The idea is to build a list of data, post it to a URL, and check there aren't any errors. This test doesn't send any data and checks that the request errors for the title and content fields:

/* @test */
public function createpagerequires_validation()
{
    $this->loginAsAdmin();
    // Post empty data to the create page route
    $response = $this->post('/admin/pages');
    // This should cause errors with the 
    // title and content fields as they aren't present
    $response->assertSessionHasErrors([
        'title',
        'content',
    ]);
}

REDIRECTS

If you have any redirects in your system - e.g. if you're not logged in but trying to access the admin, it will redirect you to the login screen - yep, you've guessed it, you can test these! The Laravel docs have a big list of all the assertions you can make, but here's a quick example.

If you try to create a page as a user rather than an admin, it should redirect you to the homepage:

/* @test */
public function ausercannotaccessthecreatepage_view()
{
    // Login as a user
    $this->actingAs(User::factory()->user()->create());
    // Simulate a GET request to the given URL
    $response = $this->get('/admin/pages/create');
    // Check the response, we should have been redirected to the homepage
    $response->assertRedirect('/');
}

DATABASE

Another way we can test the population of data is to check that once you submit it, it's in the database.

Instead of writing a hardcoded array of data for your test, you can create a factory class to generate random values using a Faker.

<?php
namespace Database\Factories;
use App\Domains\Page\Models\Page;
use Illuminate\Database\Eloquent\Factories\Factory;
class PageFactory extends Factory
{
    /**
     * The name of the factory's corresponding model
     *
     * @var string $model
     */
    protected $model = Page::class;
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'title' => $this->faker->sentence,
            'slug' => $this->faker->slug,
            'content' => $this->faker->paragraphs(rand(1, 9), true),
        ];
    }
}

The title, slug, and content fields all get random values generated by the Faker. More information about factories can be found in the Laravel docs, and the types of generators can be found in the FakerPHP documentation.

When we check the database we use a method called assertDatabaseHas(). You pass in the database table name and an array of values you expect it to have. I wouldn't recommend testing the createdat / updatedat values with this, as the time between creating the data and checking it will have changed and this will cause the test to fail. Instead I set specific columns to check against.

This is an example where I am checking an admin can create a page, by confirming the data is in the table:

/* @test */
public function admincancreate_page()
{
    $this->loginAsAdmin();
    // Get data from the Factory
    $page = Page::factory()->make()->toArray();
    // Post data to the 'create' route
    $response = $this->post('/admin/pages', $page);
    // Check the database has the data we generated with the factory
    $this->assertDatabaseHas(
        'pages',
        [
            'title' => $page['title'],
            'slug' => Str::slug($page['title']),
            'content' => $page['content'],
        ]
     );
}

SUMMARY

Automated testing is a very powerful tool that can help you build your website without worrying over the small details. The examples I've given are only basic to help you start, but there are so many ways to expand from here!

Screenshot of command line tests
Output from running the tests in the command line - everything succeeds!

Need support with your development project? Our technical team is here to help!

Written by Laura Martin
Senior Software Engineer

Laura arrived at Evoluted in the role of Junior Web Developer in 2018. Having amassed over 4 years’ previous experience working for web agencies, she’s spent time working on a wide array of websites; for organisations such as student letting providers and music festivals. In her role at Evoluted, she spends her days working on improvements for our clients’ websites.