Kritim Yantra
Apr 15, 2025
Laravel 12 continues to be a top choice for modern PHP developers, offering elegant syntax, powerful features, and a supportive ecosystem. In this blog, we'll walk through how to build a simple CRUD (Create, Read, Update, Delete) application using Test-Driven Development (TDD) β a methodology that promotes writing tests before the code.
TDD helps you:
We'll build a simple "Posts" app where users can create, read, update, and delete posts.
composer create-project laravel/laravel:^12.0 laravel-tdd-crud
)Laravel ships with PHPUnit out of the box, but you can also use Pest for a more elegant syntax.
composer require pestphp/pest --dev
php artisan pest:install
We'll stick with PHPUnit syntax here for simplicity.
Letβs write a test that ensures a post can be created.
php artisan make:test PostTest
In tests/Feature/PostTest.php
:
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\Post;
public function test_a_post_can_be_created()
{
$this->withoutExceptionHandling();
$response = $this->post('/posts', [
'title' => 'Test Post',
'content' => 'This is a test post.',
]);
$response->assertRedirect('/posts');
$this->assertDatabaseHas('posts', ['title' => 'Test Post']);
}
Run it:
php artisan test
π₯ It will fail. Now letβs write just enough code to pass it.
php artisan make:model Post -m
Update the migration in database/migrations/...create_posts_table.php
:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
Run the migration:
php artisan migrate
Update Post.php
:
protected $fillable = ['title', 'content'];
php artisan make:controller PostController
In routes/web.php
:
use App\Http\Controllers\PostController;
Route::resource('posts', PostController::class);
Update PostController
:
public function store(Request $request)
{
Post::create($request->validate([
'title' => 'required',
'content' => 'required',
]));
return redirect('/posts');
}
Now rerun the test. β It should pass!
public function test_posts_can_be_listed()
{
$post = Post::factory()->create();
$response = $this->get('/posts');
$response->assertSee($post->title);
}
In PostController.php
:
public function index()
{
$posts = Post::latest()->get();
return view('posts.index', compact('posts'));
}
Create resources/views/posts/index.blade.php
:
@foreach ($posts as $post)
<h2>{{ $post->title }}</h2>
@endforeach
β Test passes.
public function test_a_post_can_be_updated()
{
$post = Post::factory()->create();
$response = $this->put("/posts/{$post->id}", [
'title' => 'Updated Title',
'content' => 'Updated content.',
]);
$response->assertRedirect('/posts');
$this->assertDatabaseHas('posts', ['title' => 'Updated Title']);
}
public function update(Request $request, Post $post)
{
$post->update($request->validate([
'title' => 'required',
'content' => 'required',
]));
return redirect('/posts');
}
β Test passes.
public function test_a_post_can_be_deleted()
{
$post = Post::factory()->create();
$response = $this->delete("/posts/{$post->id}");
$response->assertRedirect('/posts');
$this->assertDatabaseMissing('posts', ['id' => $post->id]);
}
public function destroy(Post $post)
{
$post->delete();
return redirect('/posts');
}
β Green again!
Your PostTest.php
should now include:
RefreshDatabase
for test isolation.With Laravel 12 and TDD, building reliable applications becomes a structured and testable process. By writing tests before implementation, you can avoid bugs, document expected behavior, and ensure maintainability over time.
Ready to scale your app? Add features like authentication, tagging, and comments β all using TDD!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google