We’re moving along nicely with our test driven development of the forum. In this tutorial we will start tackling the concept of adding replies to threads. We’ll continue with developing the test case first, thinking out the pseudo code, and then adding the actual code to the application to make it all work. We’ll also take a loot at the concept of Feature Tests vs Unit Tests. Basically when testing at the Feature level, we are testing from a high level in an outside in fashion. For the first time in this series, we’ll also implement a Unit Test which is a more granular and basic level of test. The reason for this is that when a Feature Test fails, it can be difficult to find the error. If a Unit Test fails, it is usually easier to spot the problem. Let’s get started.
Refactoring Existing Feature Tests
We can start by doing a bit of refactoring on our existing Thread Tests class. We are going to rename the class to ReadThreadsTest and will also implement a setUp() method on the class to dry up the code a bit.
Refactoring in PHP Storm to rename a file and class at once.
Refactoring the test code to add a test and dry up the code.
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateFoundationTestingDatabaseMigrations;
class ReadThreadsTest extends TestCase
{
use DatabaseMigrations;
public function setUp()
{
parent::setUp();
$this->thread = factory('AppThread')->create();
}
public function test_a_user_can_browse_threads()
{
$response = $this->get('/threads');
$response->assertSee($this->thread->title);
$response->assertStatus(200);
}
public function test_a_user_can_read_a_single_thread()
{
$response = $this->get('/threads/' . $this->thread->id);
$response->assertSee($this->thread->title);
$response->assertStatus(200);
}
public function test_a_user_can_see_replies_that_are_associated_with_a_thread()
{
}
}
In the above snippet we try to highlight some of the important changes. First off, you’ll notice we now have a setUp() function, and this is done so that we do not need to make a call to factory(‘AppThread’)->create() inside of every test function. Now, we call it once, assign it to the object, and then use as needed. Next up, we can see that we need to make some quick edits to the code in the existing test functions to make use of $this->thread instead of just $thread. Finally, we added the new test of test_a_user_can_see_replies_that_are_associated_with_a_thread() and just left it blank for the time being. Let’s go ahead and run the tests just to see where we are at.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. ..R. 4 / 4 (100%) Time: 1.08 seconds, Memory: 8.00MB There was 1 risky test: 1) TestsFeatureReadThreadsTest::test_a_user_can_see_replies_that_are_associated_with_a_thread This test did not perform any assertions OK, but incomplete, skipped, or risky tests! Tests: 4, Assertions: 5, Risky: 1.
Interesting. It looks like the existing tests are passing, but that new test we added is getting labeled as Risky. It’s just an empty test so this makes sense. Now, let’s consider what we want this new test to do. The pseudo code might look something like this:
- Given there is a thread
- Given this thread had replies
- When a user visits the thread page
- The user should see replies
Translating that pseudo code into actual test code might look something like this:
public function test_a_user_can_see_replies_that_are_associated_with_a_thread()
{
$reply = factory('AppReply')->create(['thread_id' => $this->thread->id]);
$response = $this->get('/threads/' . $this->thread->id);
$response->assertSee($reply->body);
$response->assertStatus(200);
}
Running the test gives us an expected failure, since we don’t even have that code in place to support the feature yet. Basically, the following shows us that we do not see the body of the reply on the page.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. ..F. 4 / 4 (100%) Time: 1.45 seconds, Memory: 10.00MB There was 1 failure: 1) TestsFeatureReadThreadsTest::test_a_user_can_see_replies_that_are_associated_with_a_thread Failed asserting that (the html) contains "Libero aliquam officiis id. Consequatur magni deserunt nesciunt ipsum. Iste ipsam voluptate unde est. Hic aspernatur corrupti iure sit.".
Make The Test Pass!
Behold! We must now take up our task of making the test pass. Let’s see what we need to do.
First, we need to display the replies on the threads page. Let’s open up /resources/threads/show.blade.php and add the code like so:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">{{ $thread->title }}</div>
<div class="panel-body">
{{ $thread->body }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
@foreach($thread->replies as $reply)
<div class="panel panel-default">
<div class="panel-body">
{{ $reply->body }}
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
Now this is how we would want to be able to use the markup in that page. We don’t even need to run the tests to know it is going to fail since we do not have a replies method on the Thread model. In other words, we are trying to make use of a relationship on the model that has not yet been created, so let’s add that to our Model now. A thread can have many replies, so we’ll use the hasMany() relationship like so:
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Thread extends Model
{
public function path()
{
return '/threads/' . $this->id;
}
public function replies()
{
return $this->hasMany(Reply::class);
}
}
Pretty cool. Now in theory, our tests should all pass. Running them shows us this is in fact the case.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. .... 4 / 4 (100%) Time: 1.05 seconds, Memory: 8.00MB OK (4 tests, 7 assertions)
This means that if we now visit a specific thread id, we should see all the associated replies with it as well.
Making Use Of A Unit Test
We have not set up the ability to view the owner of a reply on the page yet. We can do that by adding the following code to our view file.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">{{ $thread->title }}</div>
<div class="panel-body">
{{ $thread->body }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-8 col-md-offset-2">
@foreach($thread->replies as $reply)
<div class="panel panel-default">
<div class="panel-body">
{{$reply->owner->name}} said {{ $reply->created_at->diffForHumans() }}
</div>
<div class="panel-body">
{{ $reply->body }}
</div>
</div>
@endforeach
</div>
</div>
</div>
@endsection
This introduces a good time to set up a Unit test. We can do so with the command:
vagrant@homestead:~/Code/forumio$ php artisan make:test ReplyTest --unit Test created successfully.
Now what we want to do with this Unit Test is to simply confirm that it has an owner. In other words we are testing that the relationship between a thread reply and owner is valid. The code in the test for this might look like so:
<?php
namespace TestsUnit;
use IlluminateFoundationTestingDatabaseMigrations;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
class ReplyTest extends TestCase
{
use DatabaseMigrations;
public function test_it_has_an_owner()
{
$reply = factory('AppReply')->create();
$this->assertInstanceOf('AppUser', $reply->owner);
}
}
Let’s try running the test.
vagrant@homestead:~/Code/forumio$ phpunit tests/Unit/ReplyTest PHPUnit 6.5.2 by Sebastian Bergmann and contributors. F 1 / 1 (100%) Time: 675 ms, Memory: 8.00MB There was 1 failure: 1) TestsUnitReplyTest::test_it_has_an_owner Failed asserting that null is an instance of class "AppUser". /home/vagrant/Code/forumio/tests/Unit/ReplyTest.php:17 FAILURES! Tests: 1, Assertions: 1, Failures: 1.
The test fails but it is nice and concise. We know exactly what went wrong here. We don’t yet have the method for our relationship on the Reply model. We can fix that now!
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Reply extends Model
{
public function owner()
{
return $this->belongsTo(User::class, 'user_id');
}
}
Here we add the familiar belongsTo relationship. A Reply model belongs to an owner. Also note that since we named the function owner() instead of user(), we need to explicitly state the foreign id is user_id as the second argument in the belongsTo() method. Ok, we are ready to run our unit test again. Here we go:
vagrant@homestead:~/Code/forumio$ phpunit tests/Unit/ReplyTest PHPUnit 6.5.2 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 643 ms, Memory: 8.00MB OK (1 test, 1 assertion)
Excellent! Our unit test is passing. Notice how we are running just one test here with the syntax of phpunit tests/Unit/ReplyTest. We can do this for any test we like. We can specify which test it is we want to run. This is helpful when you do not really need to run the entire suite of tests that you may have built up in the application. Given that our test here has passed, this means it should work in the browser as well. Here we visit a specific thread in the browser, and we see the replies now have an owner.
Feature Test vs Unit Test And Adding Replies To Threads Summary
Our little application is moving along quite nicely. So far we have three feature tests of test_a_user_can_browse_threads(), test_a_user_can_read_a_single_thread(), and test_a_user_can_see_replies_that_are_associated_with_a_thread(). In addition to these we have a unit test of test_it_has_an_owner() which simply confirms that a thread reply has an owner. Whether we run the full suite of tests, or any of the tests individually, they are all passing. Nice!