Create a Laravel 8 Blog - Part 7: Testing

Overview

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.

  1. 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' => '' ]); } }
  1. 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;
  1. 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')); } }

See the commit

  1. Add CommentControllerTest.php
./vendor/bin/sail artisan make:test CommentControllerTest

Add use DatabaseTransaction to ensure it rolls back any new database rows created.

  1. 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); } }
  1. 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..
If you have any feedback for me, I'd love to hear it - corrections, alternative paths, you name it! Send me an email levi@levijackson.xyz