Overview
- Part 1: Walk-through installing and configuring Laravel
- Part 2: Create the core models/tables, controllers, and views
- Part 3: Authorization/Login
- Part 4: Post CRUD
- Part 5: Comment CRUD
- Part 6: Validation
- Part 7: Testing
What do we want to test?
Let's start by outlining the things that are most important to make sure work.
Anonymous users
- Can view the blog post list
- Can view single blog posts
- Can not comment on a blog post
- Can not view "draft" posts
- Can not load "new blog post" route
- Can not load "edit blog post" route
- Can not delete blog posts
Authors
- Can add a new post
- Can edit a post
- Can delete a post
- Can view a draft blog post
Controllers
We can test response codes for the controllers to get good coverage that our authorization rules are in place.
- Add some seed data so we can simulate these scenarios.
./vendor/bin/sail artisan make:seeder PostSeeder
We'll add a test active post and a test inactive post.
# database/seeders/PostSeeder.php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; class PostSeeder extends Seeder { const POST_1_SLUG = 'laravel-test-post-1'; const POST_1_USER_ID = '1'; const POST_2_SLUG = 'laravel-test-post-2'; const POST_2_USER_ID = '2'; /** * Run the database seeds. * * @return void */ public function run() { DB::table('posts')->insert([ 'user_id' => self::POST_1_USER_ID, 'title' => 'Test Post 1', 'body' => 'Test Body 1', 'slug' => self::POST_1_SLUG, 'active' => '1', 'metaDescription' => '', 'metaTitle' => '' ]); DB::table('posts')->insert([ 'user_id' => self::POST_2_USER_ID, 'title' => 'Test Post 2', 'body' => 'Test Body 2', 'slug' => self::POST_2_SLUG, 'active' => '0', 'metaDescription' => '', 'metaTitle' => '' ]); } }
- Add
PostControllerTest.php
./vendor/bin/sail artisan make:test PostControllerTest
Add use DatabaseTransaction
to ensure it rolls back any new database rows created.
class PostControllerTest extends TestCase { use DatabaseTransactions;
- Write the tests! Working down the list from above, write tests that verify functionality.
As you are working on the tests you can run them using artisan.
./vendor/bin/sail artisan test
Anything supported by PHPUnit can be done here as well, such as --filter
.
./vendor/bin/sail artisan test --filter=testBlogPageDoesLoadAnonymousUser
# tests/Feature/PostControllerTest.php namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; use App\Models\User; use Database\Seeders\PostSeeder; use Illuminate\Foundation\Testing\DatabaseTransactions; class PostControllerTest extends TestCase { use DatabaseTransactions; /** * Anyone can view the list of blog posts * * @return void */ public function testBlogPageDoesLoadAnonymousUser(): void { $response = $this->get('/blog'); $response->assertStatus(200); } /** * Anyone can view single blog posts * * @return void */ public function testSingleBlogPostDoesLoadAnonymousUser(): void { $this->seed(PostSeeder::class); $response = $this->get('/blog/post/' . PostSeeder::POST_1_SLUG); $response->assertStatus(200); } /** * Draft posts should not load for anonymous users * * @return void */ public function testDraftBlogPostDoesNotLoadAnonymousUser(): void { $this->seed(PostSeeder::class); $response = $this->get('/blog/post/' . PostSeeder::POST_2_SLUG); $response->assertStatus(302); } /** * Draft posts SHOULD load for authors * * @return void */ public function testDraftBlogPostDoesLoadAuthor(): void { $this->seed(PostSeeder::class); $user = User::factory()->create(); $user->role = 'author'; $response = $this->actingAs($user) ->get('/blog/post/' . PostSeeder::POST_2_SLUG); $response->assertStatus(200); } /** * Anonymous users can not load new blog post page * * @return void */ public function testNewBlogPostDoesNotLoadAnonymousUser(): void { $response = $this->get('/admin/blog/post'); $response->assertStatus(302); } /** * Authors can load the new blog post page * * @return void */ public function testNewBlogPostDoesLoadAuthor(): void { $user = User::factory()->create(); $user->role = 'author'; $response = $this->actingAs($user) ->get('/admin/blog/post'); $response->assertStatus(200); } /** * Authors can save a new blog post * * @return void */ public function testNewBlogPostCreatedByAuthor(): void { $user = User::factory()->create(); $user->role = 'author'; $user->id = '1'; $data = [ 'title' => 'Test new post by author', 'metaTitle' => '', 'body' => 'a blog post', 'metaDescription' => '', 'slug' => 'test-new-post-by-author' ]; $response = $this->actingAs($user) ->post('/admin/blog/post', $data); $response->assertStatus(302); $this->assertGreaterThan(0, strpos($response->getTargetUrl(), $data['slug'])); } /** * Anonymous users can not load a blog post edit page * * @return void */ public function testEditBlogPostDoesNotLoadAnonymousUser(): void { $this->seed(PostSeeder::class); $response = $this->get('/admin/blog/post/' . PostSeeder::POST_1_SLUG); $response->assertStatus(302); } /** * Authors can load the edit blog post page * * @return void */ public function testEditBlogPostDoesLoadAuthor(): void { $this->seed(PostSeeder::class); $user = User::factory()->create(); $user->role = 'author'; $user->id = '1'; $response = $this->actingAs($user) ->get('/admin/blog/post/' . PostSeeder::POST_1_SLUG); $response->assertStatus(200); } /** * Authors can save an edit to a blog post * * @return void */ public function testEditBlogPostSavedByAuthor(): void { $this->seed(PostSeeder::class); $user = User::factory()->create(); $user->role = 'author'; $user->id = '1'; $data = [ 'title' => 'Test new post by author', 'metaTitle' => '', 'body' => 'a blog post', 'metaDescription' => '', 'slug' => 'test-edit-post-by-author' ]; $response = $this->actingAs($user) ->put('/admin/blog/post/' . PostSeeder::POST_1_SLUG, $data); $response->assertStatus(302); $this->assertGreaterThan(0, strpos($response->getTargetUrl(), $data['slug'])); } /** * Authors can delete posts * * @return void */ public function testAuthorCanDeletePost(): void { $this->seed(PostSeeder::class); $user = User::factory()->create(); $user->role = 'author'; $user->id = '1'; $data = [ 'title' => 'Test post by author', 'metaTitle' => '', 'body' => 'a blog post', 'metaDescription' => '', 'delete' => 1 ]; $response = $this->actingAs($user) ->put('/admin/blog/post/' . PostSeeder::POST_2_SLUG, $data); $response->assertStatus(302); $this->assertGreaterThan(0, strpos($response->getTargetUrl(), '/admin/blog/post')); } }
- Add
CommentControllerTest.php
./vendor/bin/sail artisan make:test CommentControllerTest
Add use DatabaseTransaction
to ensure it rolls back any new database rows created.
- Write the tests!
namespace Tests\Feature; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\WithFaker; use Tests\TestCase; use App\Models\User; use Database\Seeders\PostSeeder; use Illuminate\Foundation\Testing\DatabaseTransactions; class CommentControllerTest extends TestCase { use DatabaseTransactions; /** * Anonymous users can not comment on a post * * @return void */ public function testAnonymousUsersCanNotComment(): void { $this->seed(PostSeeder::class); $response = $this->post('/admin/blog/comment', ['post_id' => '1', 'comment' => 'test comment']); $response->assertStatus(403); } /** * Anonymous users can not delete a comment on a post * * @return void */ public function testAnonymousUsersCanNotDeleteComment(): void { $this->seed(PostSeeder::class); $response = $this->delete('/admin/blog/comment/1'); $response->assertStatus(403); } }
- Run the tests
./vendor/bin/sail artisan test
PASS Tests\Feature\CommentControllerTest ✓ anonymous users can not comment ✓ anonymous users can not delete comment PASS Tests\Feature\PostControllerTest ✓ blog page does load anonymous user ✓ single blog post does load anonymous user ✓ draft blog post does not load anonymous user ✓ draft blog post does load author ✓ new blog post does not load anonymous user ✓ new blog post does load author ✓ new blog post created by author ✓ edit blog post does not load anonymous user ✓ edit blog post does load author ✓ edit blog post saved by author ✓ author can delete post Tests: 14 passed Time: 3.35s
Roundup
That's it for now. If you want to take this even further you can think about some of these things:
- Adding a WYSIWYG
- Adding an "Author" page that lists all of their posts
- Adding ability to login with another provider like Google, Facebook, LinkedIn, etc..