Folder structure
In this article, we will explore how to organize tests effectively, using the example of a feature that allows users to write and publish a blog post. Consider an API endpoint that allows authenticated users to add a new post. There are various way to sturcture your tests, for instance you could create a Feature/PostTest
class and write all the test realted to a feature about creating a post in this class.
Benefits
- Each feature has its own dedicated test class.
- All tests related to a specific feature are grouped within a single class.
- Maintains a flat folder structure without nested levels, simplifying organization.
- Ideal for small codebases and teams with fewer members.
Drawbacks of this structure:
- In larger codebases, it becomes challenging to locate specific test cases related to the code.
- All the test cases related to one feature will be in single file
- Difiiculties to mange in larger team or larger code base.
I personally prefer this approach when working alone or in a small team. However, in larger codebases, it can be challenging to group and identify tests for each feature effectively, making this structure harder to maintain.
Another approach of organizaing folder strucure is to create a Test class that mirrors the directory of the corrosponding code files but resides within the tests
folder, for example, if a controller is located at src/application/controllers/BlogController.php
the test for this controller will be written in tests/Feature/application/controllers/BlogControllerTest.php
. In this setup, the Feature
prefix refers to complete end-to-end
tests.
Benefits
- Simplifies locating tests for a specific piece of code and vice versa.
- Enhances organization, making tests easily discoverable, especially in larger teams.
Test Setup
To create a new Laravel application, use the command laravel new laravel-tdd
. During the setup, you'll be prompted to select a testing framework. For simplicity, I chose PHPUnit as the testing framework and sqlite as the database. Laravel includes a basic test setup by default, and you can run your tests using php artisan test
. By default, it runs and passes two tests.
Database Setup
Consider a feature, where user should able to write a blog post and publish it. Consider a blog contains only title and body for now for simplicity. To implement this feature we need database table, Model and Controller, so Let's quickly create a database model, migrations, factory and controller with this command php artisan make:model Blog -mfcr
. The following files are created below:
app/Http/Controllers/BlogController.php
app/Models/Blog.php
database/migrations/2025_01_22_195944_create_blogs_table.php
database/factories/BlogFactory.php
Let's add title
and body
into our migration.
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('blogs', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->longText('body')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('blogs');
}
};
we need to add these fields are fillable in our model:
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Blog extends Model
{
/** @use HasFactory<\Database\Factories\BlogFactory> */
use HasFactory;
protected $fillable = [
'title',
'body'
];
}
We added the following route in web.php
use App\Http\Controllers\BlogController;
Route::post('blogs', [BlogController::class, 'store'])->name('blogs.store');
and we also implemented the store
method in BlogController
as:
class BlogController extends Controller
{
...
public function store(Request $request)
{
Blog::query()->create($request->all());
return redirect()->back()->with('success', 'Blog created successfully');
}
...
}
The code above processes data from a POST request and saves it to the blogs
database table. Now, let’s write an integration test to ensure the feature works as expected by verifying:
- A user can successfully submit a request.
- The submitted data is correctly stored in the database.
Create a test class Tests\Feature\Http\Controllers\BlogControllerTest
. A basic test for this feature would look like this:
<?php
namespace Tests\Feature\Http\Controllers;
use Tests\TestCase;
class BlogControllerTest extends TestCase
{
public function testUserCanCreateABlogPost(): void
{
$this->post(route('blog.store'), [
"title" => "Blog title",
"body" => "Blog description",
]);
$this->assertDatabaseHas("blogs", [
"title" => "Blog title",
"body" => "Blog description",
]);
}
}
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: blogs (Connection: sqlite, SQL: select exists(select * from "blogs" where ("title" = Blog title and "body" = Blog description)) as "exists")
We got exception as we haven't setup our database yet. The easiest things to do is uncomment these two lines in phpunit.xml
file
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
We are telling our PHPUnit to use in memory sqlite as our database. and add use \Illuminate\Foundation\Testing\RefreshDatabase;
as trait in our Tests\Feature\Http\Controllers\BlogControllerTest
class. Our complete class would look like:
<?php
namespace Tests\Feature\Http\Controllers;
use Tests\TestCase;
class BlogControllerTest extends TestCase
{
use \Illuminate\Foundation\Testing\RefreshDatabase;
public function testUserCanCreateABlogPost(): void
{
$this->post(route('blogs.store'), [
"title" => "Blog title",
"body" => "Blog description",
]);
$this->assertDatabaseHas("blogs", [
"title" => "Blog title",
"body" => "Blog description",
]);
}
}
Congratulations our first test passed.
Conclusion
In this guide, we implemented a feature that allows users to write and publish blog posts. We structured the project step by step, from creating the necessary database table, model, and controller to setting up routes and implementing the logic to handle POST requests.
We also demonstrated how to write an integration test to verify the functionality of the feature, ensuring that:
- A user can successfully submit a request.
- The submitted data is correctly stored in the database.
Through this process, we encountered and resolved common challenges, such as setting up the database for testing. By using an in-memory SQLite database and the RefreshDatabase trait, we streamlined the test environment to ensure reliable and efficient test execution.
This approach highlights the importance of structured development and testing in Laravel, ensuring that features are both functional and maintainable. By passing our first test, we’ve laid the foundation for building robust, well-tested features in our application.
Top comments (0)