Building a Laravel 12 CRUD App with Test-Driven Development (TDD)
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.
🧠 Why TDD?
TDD helps you:
- Write cleaner, more maintainable code
- Catch bugs early
- Document features through tests
- Refactor with confidence
We'll build a simple "Posts" app where users can create, read, update, and delete posts.
🛠️ Prerequisites
- PHP 8.2+
- Composer
- Laravel 12 (
composer create-project laravel/laravel:^12.0 laravel-tdd-crud) - A database (MySQL or SQLite recommended)
🧪 Step 1: Setting Up PHPUnit & Pest (Optional)
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.
📁 Step 2: Create the Post Model and Migration
Let’s write a test that ensures a post can be created.
Create the test
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.
⚙️ Step 3: Create the Post Model and Migration
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'];
🧩 Step 4: Create the Controller & Routes
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!
📖 Step 5: Read (List and View a Single Post)
Add Test
public function test_posts_can_be_listed()
{
$post = Post::factory()->create();
$response = $this->get('/posts');
$response->assertSee($post->title);
}
Add Route and Method
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.
✏️ Step 6: Update Post
Add Test
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']);
}
Controller Update Method
public function update(Request $request, Post $post)
{
$post->update($request->validate([
'title' => 'required',
'content' => 'required',
]));
return redirect('/posts');
}
✅ Test passes.
❌ Step 7: Delete Post
Add Test
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]);
}
Controller Delete Method
public function destroy(Post $post)
{
$post->delete();
return redirect('/posts');
}
✅ Green again!
🧪 Final Test File Overview
Your PostTest.php should now include:
- test_a_post_can_be_created
- test_posts_can_be_listed
- test_a_post_can_be_updated
- test_a_post_can_be_deleted
📈 Best Practices
- Use factories for cleaner test data.
- Keep tests small and focused.
- Use
RefreshDatabasefor test isolation. - Use Pest if you want a more expressive syntax.
🚀 Conclusion
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!
Projects to explore next
Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.
Flask Authentication System (Free Version) - Kritim Yantra
🛠 Features✔ Google & GitHub Authentication✔ User Registration & Login✔ Profile/Dashboard Access✔ Secure & Mi...
Flask Blog CMS with Admin Dashboard — Python Source Code
A Flask-based blogging website with a built-in admin dashboard. Manage articles, images, categories, tags and featured p...
Laravel 12 + ReactJS Starter Kit: Add Multi-Language Support (Arabic, English, Spanish) with Blog CRUD
🚀 Installation & Setup Download & Extract Get the ZIP from kritimyantra.com and extract it. Includes preconfigu...
Comments
No comments yet. Be the first to comment.
Sign in to join the discussion.