Since we are doing test driven development, we need to actually create tests for features before we even build them. In this tutorial, that is exactly what we will do. The first thing, or feature, that our forum project will have is that a user can view threads. We can navigate to the tests folder and rename the boilerplate ExampleTest.php to ThreadsTest.php. Before we do that however, we will need to set up our testing environment, so that will be the first step.
Configure In Memory Sqlite Database Testing
For this step we need to configure the phpunit.xml file. We can specify both the DB_CONNECTION
and DB_DATABASE
values as shown.
./tests/Feature
./tests/Unit
./app
What this is telling Laravel is that when we run tests in PHP Unit, it will use the in memory Sqlite option.
Leverage The Database Migrations Trait
In our tests, we can use the database migrations trait for testing. This way for every single test that runs, the database will be migrated if needed, data populated or removed, and once the test is complete everything will get rolled back. This is pretty neat actually. Here is the code that is powering this migration feature.
<?php
namespace IlluminateFoundationTesting;
use IlluminateContractsConsoleKernel;
trait DatabaseMigrations
{
/**
* Define hooks to migrate the database before and after each test.
*
* @return void
*/
public function runDatabaseMigrations()
{
$this->artisan('migrate:fresh');
$this->app[Kernel::class]->setArtisan(null);
$this->beforeApplicationDestroyed(function () {
$this->artisan('migrate:rollback');
RefreshDatabaseState::$migrated = false;
});
}
}
Create The First Feature Test
With these items in place now, we can create the first test in our ThreadsTest.php file. We want users to be able to browse threads so we will create this test named as a_user_can_browse_threads. This is simply a function of the ThreadsTest class. Here is the code we can start with.
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateFoundationTestingDatabaseMigrations;
class ThreadsTest extends TestCase
{
use DatabaseMigrations;
/**
* A user can browse threads
*/
public function a_user_can_browse_threads()
{
$response = $this->get('/threads');
$response->assertStatus(200);
}
}
So far all this test is saying is, when a user visits the /threads endpoint, the response should be a 200 OK. So we can run the test.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. W. 2 / 2 (100%) Time: 646 ms, Memory: 6.00MB There was 1 warning: 1) Warning No tests found in class "TestsFeatureThreadsTest". WARNINGS! Tests: 2, Assertions: 1, Warnings: 1.
Hmm, I expected the failure, but not that one. Let’s see if we can fix this. Ok, Interesting. It looks like I needed to use a different naming convention on the function test name. Perfect, we can update our test name from a_user_can_browse_threads to test_a_user_can_browse_threads and we should be good to go. Here is the updated Test case.
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateFoundationTestingDatabaseMigrations;
class ThreadsTest extends TestCase
{
use DatabaseMigrations;
/**
* A user can browse threads
*/
public function test_a_user_can_browse_threads()
{
$response = $this->get('/threads');
$response->assertStatus(200);
}
}
Let us once again run our test.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. F. 2 / 2 (100%) Time: 789 ms, Memory: 8.00MB There was 1 failure: 1) TestsFeatureThreadsTest::test_a_user_can_browse_threads Expected status code 200 but received 404. Failed asserting that false is true. /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:78 /home/vagrant/Code/forumio/tests/Feature/ThreadsTest.php:19 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
Ah ha. It fails. We get an error of a bad 404 response. The 404 response means not found, as we learned about in our http status codes tutorial. We expected this test to fail because we don’t even have a route for the /threads endpoint. So with testing, we pretty much always have the test fail at first and then keep fixing small things until the test finally passes. So let’s try to fix things up and make this test pass. First, we can add the route.
<?php
Route::get('/', function () {
return view('welcome');
});
Route::get('/threads', 'ThreadsController@index');
Real quick, it looks like when we first created our controllers they were not in the plural form. We can fix that with a quick refactor in PHP Storm. Make sure to complete this step if you are following along.
Refactor ThreadController to ThreadsController
Refactor ReplyController to RepliesController
With these refactors in place, let’s just add a quick snippet to the index method in our ThreadsController file so we can run the test again. We’ll simply return the string of ‘hi’, and this should be enough to generate a 200 OK status back to us.
class ThreadsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return IlluminateHttpResponse
*/
public function index()
{
return 'hi';
}
Let’s run the test again.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. F. 2 / 2 (100%) Time: 796 ms, Memory: 10.00MB There was 1 failure: 1) TestsFeatureThreadsTest::test_a_user_can_browse_threads Expected status code 200 but received 500. Failed asserting that false is true. /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:78 /home/vagrant/Code/forumio/tests/Feature/ThreadsTest.php:19 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
What the?! Things have gone from bad to worse. Initially we had a failure due to a 404 not found message. Now we are busting out a full status code 500, or in other words, a catastrophic failure. When in doubt, clear the cache, as well as do a composer dump.
vagrant@homestead:~/Code/forumio$ php artisan cache:clear Cache cleared successfully. vagrant@homestead:~/Code/forumio$ composer dump Generating optimized autoload files > IlluminateFoundationComposerScripts::postAutoloadDump > @php artisan package:discover Discovered Package: fideloper/proxy Discovered Package: laravel/tinker Package manifest generated successfully.
Let’s run that test one more time.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 669 ms, Memory: 8.00MB OK (2 tests, 2 assertions)
BOOM! For the first time so far, we get a passing test. LOL. Of course we know that we actually want the test to do more than just confirm a status 200 code on the response and we will tackle that shortly. At this time, let’s use artisan to generate some auth scaffolding so we can use some of that generated html in our project. We covered this topic in the Laravel user registration tutorial.
vagrant@homestead:~/Code/forumio$ php artisan make:auth Authentication scaffolding generated successfully.
This creates great looking login and register pages that are fully functional for you. As you can see, they look quite nice.
Now what we are going to do is create the view file we need for our threads test, and borrow some of the markup that is created with the auth files we just created. So in the /resources/views/threads directory we can create index.blade.php. What we can now do is open the home.blade.php file which was generated for us, then copy paste into our file while making some modifications so that it gives us just a basic work up for our view.
@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">Dashboard</div>
<div class="panel-body">
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
You are logged in!
</div>
</div>
</div>
</div>
</div>
@endsection
While we are at it, let’s also fix the index() method in our ThreadsController. What we can do to start is to simply fetch all threads from the database, pass them to the view, and render them out on the page. Here is our updated controller.
<?php
namespace AppHttpControllers;
use AppThread;
use IlluminateHttpRequest;
class ThreadsController extends Controller {
/**
* Display a listing of the resource.
*
* @return IlluminateHttpResponse
*/
public function index() {
$threads = Thread::latest()->get();
return view( 'threads.index', compact( 'threads' ) );
}
/**
* Show the form for creating a new resource.
*
* @return IlluminateHttpResponse
*/
public function create() {
//
}
/**
* Store a newly created resource in storage.
*
* @param IlluminateHttpRequest $request
*
* @return IlluminateHttpResponse
*/
public function store( Request $request ) {
//
}
/**
* Display the specified resource.
*
* @param AppThread $thread
*
* @return IlluminateHttpResponse
*/
public function show( Thread $thread ) {
//
}
/**
* Show the form for editing the specified resource.
*
* @param AppThread $thread
*
* @return IlluminateHttpResponse
*/
public function edit( Thread $thread ) {
//
}
/**
* Update the specified resource in storage.
*
* @param IlluminateHttpRequest $request
* @param AppThread $thread
*
* @return IlluminateHttpResponse
*/
public function update( Request $request, Thread $thread ) {
//
}
/**
* Remove the specified resource from storage.
*
* @param AppThread $thread
*
* @return IlluminateHttpResponse
*/
public function destroy( Thread $thread ) {
//
}
}
With everything in place, we can once again run our tests in PHP Unit.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 1.16 seconds, Memory: 8.00MB OK (2 tests, 2 assertions)
Sweet! We are passing like a boss. That means we can try loading that page in the browser and it should work. Let’s try it out.
Making Tests More Granular
So we can see that everything is working so far both from our test, and in the browser. The test as it stands now only checks for a 200 status code. Pretty much if any page loads, no matter what it is, you’ll get a 200. That is not incredibly helpful. We likely also should assert that the test verifies correct output on the page and so forth. Let’s adjust our test a bit here.
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateFoundationTestingDatabaseMigrations;
class ThreadsTest extends TestCase
{
use DatabaseMigrations;
/**
* A user can browse threads
*/
public function test_a_user_can_browse_threads()
{
$thread = factory('AppThread')->create();
$response = $this->get('/threads');
$response->assertSee($thread->title);
$response->assertStatus(200);
}
}
This looks a little more realistic now. In our test function, we now create a thread and load it to the test database. After this, we send a get request to the /threads endpoint and check the response. This time around, we confirm or assert, that we see the actual title of the thread on the page. We also continue to assert that we receive a 200 OK status message. We can run the tests again and see all assertions are working correctly which is nice.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 1.7 seconds, Memory: 8.00MB OK (2 tests, 3 assertions)
Feature Test Two
Ok I’m feeling pretty good about the first feature being set up with tests, and built out to work correctly. A user can browse threads feature is tackled. Now, let’s create a new feature. Users should also be able to view a single thread. Following test driven development, we will start creating the test, before we even start building out that feature. We can add the following code to our ThreadsTest class to get this going.
<?php
namespace TestsFeature;
use TestsTestCase;
use IlluminateFoundationTestingRefreshDatabase;
use IlluminateFoundationTestingDatabaseMigrations;
class ThreadsTest extends TestCase {
use DatabaseMigrations;
/**
* A user can browse threads
*/
public function test_a_user_can_browse_threads() {
$thread = factory( 'AppThread' )->create();
$response = $this->get( '/threads' );
$response->assertSee( $thread->title );
$response->assertStatus( 200 );
}
public function test_a_user_can_read_a_single_thread() {
$thread = factory( 'AppThread' )->create();
$response = $this->get( '/threads/' . $thread->id );
$response->assertSee( $thread->title );
$response->assertStatus( 200 );
}
}
Much like the first test, we now test browsing a specific thread. So we create a new thread, visit the /threads/{id} endpoint, confirm we see the title, and confirm we get a status 200. We know this will fail if we test this right now but let’s go ahead and test it anyway.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. .F. 3 / 3 (100%) Time: 1.35 seconds, Memory: 8.00MB There was 1 failure: 1) TestsFeatureThreadsTest::test_a_user_can_read_a_single_thread Failed asserting that 'n n n n n n nPage Not Found n n n n n n n n nnn n n ' contains "Praesentium corrupti quia excepturi ea ut placeat quo.". /home/vagrant/Code/forumio/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:236 /home/vagrant/Code/forumio/tests/Feature/ThreadsTest.php:27 FAILURES! Tests: 3, Assertions: 4, Failures: 1.nnn Sorry, the page you are looking for could not be found.n
Whoa. That’s a pretty good blow up right there. What happens here, is that since we are trying to assert a particular string of text on the page, we see this big blob of html that gets returned in the test results. If you look closely you can see though that the main message is Failed asserting that (the html) contains “Praesentium corrupti quia excepturi ea ut placeat quo.”. Ok fair enough, let’s build out the plumbing we need to make this test pass.
We need a route for this feature so let’s add one now.
<?php
Route::get('/', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', 'HomeController@index')->name('home');
Route::get('/threads', 'ThreadsController@index');
Route::get('/threads/{thread}', 'ThreadsController@show');
We also need the show() method to be built out. We can do that like so for the show() method in the ThreadsController. Note that since we are injecting a model as a parameter to the show method, Laravel will work a little bit of magic with Route Model Binding.
public function show( Thread $thread ) {
return view( 'threads.show', compact( 'thread' ) );
}
Finally, we need a view file to display the results. Go ahead and create the show.blade.php file inside of the resources/views/threads directory. We can populate the file 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>
@endsection
Everything is now in place for our second feature of a user can view a single thread. Let’s run our tests.
vagrant@homestead:~/Code/forumio$ phpunit PHPUnit 6.5.2 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 1.4 seconds, Memory: 8.00MB OK (3 tests, 5 assertions)
Cool! It looks like we are passing. This means viewing a single thread in the browser should also work. Loading up a thread with the lucky number 7 for an id in the browser does work just great.
Adding Links Using a path() function
The final thing we can do in this tutorial is to add links to the index view so that when a user is browsing all threads, he or she can click a link to view a single thread. Let’s modify our index.blade.php file for threads 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">Forum Threads</div>
<div class="panel-body">
@foreach($threads as $thread)
<article>
<h4>
<a href="{{ $thread->path() }}">{{ $thread->title }}</a>
</h4>
<div class="body">{{ $thread->body }}</div>
</article>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection
That little path() method is a trick to create dynamic url’s automatically. If we try to load this in the browser right now, things are going to break. We need to add that function to our Thread model so that it works. Open up Thread.php and populate like so.
<?php
namespace App;
use IlluminateDatabaseEloquentModel;
class Thread extends Model
{
public function path()
{
return '/threads/' . $this->id;
}
}
That will do the trick, and we can see that viewing all threads and then clicking to view more is working great.
How To Create A Feature Test In Laravel Summary
This tutorial got us moving just a little bit further along with creating new features and making sure to have tests to support those features as we go. Great work in setting up the ability for a user to browse all threads and the associated test as well as creating the ability for a user to browse a single thread with the associated test as well.