**_Note: At the time of writing this article I'm using Laravel 9.5.1_**
The thought of “Test Driven Development” or just writing tests for your Laravel application can feel daunting, especially with the vast number of features PHPUnit has to offer. The truth is, once you uncover the basics and write your first few tests, you’ll realise that a handful of methods and assertions can get you a long way and give you confidence in your application. Let me show you.
First, we’ll start by setting up a dummy application. Of course, it’ll be a blogging website.
```bash
laravel new blogger
```
Once our application scaffold has finished, we’ll change a few settings in our **_phpunit.xml_** file. Uncomment the following lines:
```xml
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
```
This just means we will be using an in-memory database when running our tests and interacting with database entries.
Next, we’ll quickly set up our models, migrations and factories. If you’ve never used factories before, they provide a handy way to quickly spin up model instances to use in a test:
```bash
php artisan make:model Article -mcf
```
The **_-mcf_** is short for **_"make me a migration, controller and a factory for this model"_**. Let’s also add some fields to our migration and make sure to update our models `$fillable` property:
```php
// database/migrations/2022_03_18_202455_create_articles_table.php
Schema::create('articles', function (Blueprint $table) {
$table->id();
$table->string('title')->unique();
$table->string('slug')->unique();
$table->text('content');
$table->timestamps();
});
```
```php
// app/Models/Article.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content',
'slug',
'description',
];
}
```
```php
// database/factories/ArticleFactory.php
<?php
namespace Database\Factories;
use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Article>
*/
class ArticleFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
$title = $this->faker->sentence();
return [
'title' => $title,
'slug' => Str::slug($title),
'content' => $this->faker->paragraph(),
];
}
}
```
Now we we can run our migrations using artisan:
```bash
php artisan migrate
```
_Note: I’m using a sqlite database so that I don’t need to create a MySQL database locally. To do this I created a database.sqlite file in my `database` directory, then updated `DB_CONNECTION=sqlite` in my .env file. Laravel will automatically look in your database directory for the sqlite file. **This is not advised for live projects.**_
Now we can start writing some tests. For this article we’re looking at TDD (test driven development), so we will start by writing tests against functionality **_we would like_** in our application, then write the code to make our tests pass. You can structure your tests how you find most comfortable, but I like to match the folder structure for the file I’m testing. In this case, we will be testing logic in our app/Http/Controllers/ArticleController.php file, so I’ll create a test with the same path (omitting the `app` directory), adding `test` onto the end:
```bash
php artisan make:test Http/Controllers/ArticleControllerTest
```
In that file, we can discard the example test that Laravel provides us, and pull in the `RefreshDatabase` trait. This trait will tell our database to reset between each test, ensuring data isn’t leaked between tests.
Next, we can write the template for our first test. We’re going to start by asserting that we can create an article in our application.
Our file now looks like so:
```php
// tests/Feature/Http/Controllers/ArticleControllerTest.php
<?php
namespace Tests\Feature\Http\Controllers;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ArticleController extends TestCase
{
use RefreshDatabase;
/** @test */
public function an_article_can_be_created()
{
//
}
}
```
Going with the REST API methodology, I’d like to send data via a **_POST_** method to an `/articles` endpoint, which will in turn validate the request, store the article in our database and send back some kind of response.
Our first test:
```php
// tests/Feature/Http/Controllers/ArticleControllerTest.php
/** @test */
public function an_article_can_be_created()
{
// given we have some data to save
$data = [
'title' => 'The title of the article',
'slug' => 'the-title-of-the-article',
'content' => 'The content of the article',
];
// when we send the data to the /articles endpoint
$response = $this->post('/articles', $data);
// then we should get a 201 response
$response->assertStatus(201);
// we expect Article::all() to return an array with one item
$this->assertCount(1, Article::all());
// we expect the database to have one record
$this->assertDatabaseHas('articles', $data);
}
```
Obviously, when running the test it will fail...
```bash
php artisan test --filter=ArticleControllerTest
FAIL Tests\Feature\Http\Controllers\ArticleControllerTest
⨯ an article can be created
---
• Tests\Feature\Http\Controllers\ArticleControllerTest > an article can be created
Expected response status code [201] but received 404.
Failed asserting that 201 is identical to 404.
```
This makes sense seeing as we haven’t written any code to allow this functionality in our app. The beauty of TDD is that it guides us to the next step in achieving the functionality we desire. “**_Expected response status code [201] but received 404_**”. This indicates that we don’t have a route setup for this endpoint, so let’s add one.
```php
// routes/web.php
<?php
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::post('/articles', [ArticleController::class, 'store'])->name('articles.store');
```
Now when we rerun the test, we see a new error:
```bash
BadMethodCallException: Method App\Http\Controllers\ArticleController::store does not exist.
```
Ok...so we need to add the store method in `ArticleController`:
```php
// app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function store()
{
//
}
}
```
Now our test says:
```bash
Expected response status code [201] but received 200.
```
This is good, it means the test is hitting the endpoint and is being directed to the store method, but not receiving a 201 response. In order to make our test happy, we can just return a 201 response (fully aware we still aren’t doing anything else in this method, but our test will tell us where to go next):
```php
// app/Http/Controllers/ArticleController.php
public function store()
{
return response()->json([
'message' => 'Article created successfully',
], 201);
}
```
Rerun the test:
```bash
FAIL Tests\Feature\Http\Controllers\ArticleControllerTest
⨯ an article can be created
---
• Tests\Feature\Http\Controllers\ArticleControllerTest > an article can be created
Failed asserting that actual size 0 matches expected size 1.
```
Great, the response errors have gone and we’ve moved onto the next chunk of logic we need to implement - saving the data to the database. To start with, let’s just naively store whatever is sent to the endpoint in the database. Generally this is bad for many reasons, but we set up the **_$fillable_** property in our model and remember we just want our tests to pass right now. Once our tests pass, we can refactor and add functionality knowing that our tests will tell us if we have broken any functionality. Back to our store method:
```php
// app/Http/Controllers/ArticleController.php
public function store(Request $request)
{
Article::create($request->all());
return response()->json([
'message' => 'Article created successfully',
], 201);
}
```
Rerun the tests...
```php
PASS Tests\Feature\Http\Controllers\ArticleControllerTest
✓ an article can be created
Tests: 1 passed
Time: 0.09s
```
Great, passing tests! We now have peace of mind that when we extend the functionality our of store method, the tests will tell us if we’ve broken anything.
To improve user experience and also making sure we are being strict with what is stored in our database, we can write a new test to assert that we are validating the data before saving to the database.
```php
// tests/Feature/Http/Controllers/ArticleControllerTest.php
/** @test */
public function an_article_is_validated_before_being_saved()
{
// given we have some incorrect data to save (missing title)
$data = [
'slug' => '',
'content' => '',
];
// when we send the data to the /articles endpoint
$response = $this->post('/articles', $data);
// we expect the response to have errors
$response->assertSessionHasErrors('title');
// we expect Article::all() to return an array with zero items
$this->assertCount(0, Article::all());
// or
// we expect the database to have zero records
$this->assertDatabaseMissing('articles', $data);
}
```
Of course, it fails...
```bash
FAIL Tests\Feature\Http\Controllers\ArticleControllerTest
⨯ an article is validated before being saved
---
• Tests\Feature\Http\Controllers\ArticleControllerTest > an article is validated before being saved
Session is missing expected key [errors].
Failed asserting that false is true.
```
Let’s add some validation to our store method with the confidence that our first test will fail if we break any functionality.
```php
// app/Http/Controllers/ArticleController.php
public function store(Request $request)
{
$attributes = $request->validate([
'title' => ['required', 'string'],
'slug' => ['required'],
'content' => ['required', 'string'],
]);
Article::create($attributes);
return response()->json([
'message' => 'Article created successfully',
], 201);
}
```
Now we can run both of our tests with `php artisan test`:
```bash
PASS Tests\Feature\Http\Controllers\ArticleControllerTest
✓ an article can be created
✓ an article is validated before being saved
Tests: 2 passed
Time: 0.11s
```
Great! The last thing we will have a look into is how to utilise model factories in your tests. If we are testing situations where our database should have 10, 20 or even 100 entries, we don’t want to manually have to populate that data. Factories allow us to quickly generate data for the database. Since we created an ArticleFactory earlier on, we can put that to use in our next test.
```php
// tests/Feature/Http/Controllers/ArticleControllerTest.php
/** @test */
public function an_article_can_be_updated()
{
// given we have an article to update
// this will generate a new article and store it in our test database
$article = Article::factory()->create();
// when we send the data to the /articles/{id} endpoint
$response = $this->put('/articles/' . $article->id, [
'title' => 'The title of the article',
'slug' => 'the-title-of-the-article',
'content' => 'The content of the article',
]);
// we expect the database to have the updated record
$this->assertDatabaseHas('articles', [
'id' => $article->id,
'title' => 'The title of the article',
'slug' => 'the-title-of-the-article',
'content' => 'The content of the article',
]);
}
```
Again, this test will fail as we don’t have any functionality to allow us to update an article. Let’s quickly add that.
```php
// routes/web.php
<?php
use App\Http\Controllers\ArticleController;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
Route::post('/articles', [ArticleController::class, 'store'])->name('articles.store');
Route::put('/articles/{article}', [ArticleController::class, 'update'])->name('articles.update');
```
```php
// app/Http/Controllers/ArticleController.php
<?php
namespace App\Http\Controllers;
use App\Models\Article;
use Illuminate\Http\Request;
class ArticleController extends Controller
{
public function store(Request $request)
{
$attributes = $request->validate([
'title' => ['required', 'string'],
'slug' => ['required'],
'content' => ['required', 'string'],
]);
Article::create($attributes);
return response()->json([
'message' => 'Article created successfully',
], 201);
}
public function update(Article $article, Request $request)
{
$attributes = $request->validate([
'title' => ['sometimes', 'string'],
'slug' => ['sometimes'],
'content' => ['sometimes', 'string'],
]);
$article->update($attributes);
return response()->json([
'message' => 'Article updated successfully',
], 200);
}
}
```
Great, all of our tests are passing again.
```bash
PASS Tests\Feature\Http\Controllers\ArticleControllerTest
✓ an article can be created
✓ an article is validated before being saved
✓ an article can be updated
Tests: 3 passed
Time: 0.13s
```
Hopefully this article shows you that with just a few of the basics, we can test a lot of functionality in our applications. We can test responses from endpoints, whether our database has anything saved/not saved, whether Eloquent gives us any collection data and even whether the session has any errors, whilst utilising factories to speed up our testing.