Twitter From Scratch Using Laravel: Our First Test

In our previous lesson we've learned how to setup our database. In this lesson we are going to setup our testing environment and make our first test. We're going to make a test for displaying user tweets.
When it comes testing it's not advisable to use our local database because it might be populated with tons of data related only for testing, instead we're going to use another database for our testing environment. We can do that by specifying our
DB_CONNECTION
and DB_DATABASE
in our phpunit.xml
file like so.
...
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
...
So, basically we're telling Laravel to use a sqlite
database connection and use in-memory database instead of a file. In this way our tests will be a little bit faster.Next, we make sure our new configuration settings are taken into account by clearing the configuration cache like so.
php artisan config:clear
Now, let's create a feature test for our user. We can use the
make:test
Artisan command for that like so.
php artisan make:test UserTest
This will create a UserTest.php
in the tests/Feature
directory.Next, let's make a test that a user can view a tweet. So, what is the first step? First, is always remember to name your test method as humanly readable as possible because more or less the one who will read your test is probably human 😄. So, in this case maybe it would make sense if we name our method as
a_user_can_view_a_tweet
.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class UserTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_user_can_view_a_tweet()
{
}
}
Also, when dealing with database in our test we have to use DatabaseMigrations
trait. What it does is it migrates everything to the database first, before running the actual test and rolled everything back once the test is done.Next, let's run phpunit like so:
phpunit
We should get green because there is nothing yet in our a_user_can_view_a_tweet
method.Now, let's add some logic in the
a_user_can_view_a_tweet
method. If you come to think about it, we are testing that a user can view a tweet and we know for a fact that everything in twitter is public. So, in this case it doesn't really matter whether the user is logged in or not. All we need for now is a tweet. We can use model factory for that like so:
...
$tweet = factory('App\Tweet')->create();
...
What this does is it persist one tweet to the tweets
table and one user to the users
table as well. When we run phpunit
again we should still get green.Next, let's simulate a user viewing the tweet and assert that the body of the tweet is displayed in the browser:
...
$tweet = factory('App\Tweet')->create();
$response = $this->get($tweet->url);
$response->assertSee($tweet->body);
...
Now, when we run phpunit
again, it should failed which is a good thing because it means our test is working the way we expect. Remember that we are not touching our code base yet, well, at least in terms of our model, views and controller.Now, let's make our test back to green again. First, we know that the value of
$tweet->url
is null right? Let's fix that first, and you know what? this is a good candidate for unit testing. So, let's make a unit test for that like so:
php artisan make:test TweetTest --unit
Adding a --unit
flag means create a test in the tests/Unit
directory.Basically it's the same class structure with feature test, but this time we're going to zoom in a little bit to unit level. Meaning we're not going to use any http request when it comes to unit testing unless your test is required to access an api or something.
Next, let's make an assertion that a
$tweet->url
must not be null like so:
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\DatabaseMigrations;
class TweetTest extends TestCase
{
use DatabaseMigrations;
/** @test */
public function a_tweet_url_must_not_be_null()
{
$tweet = factory('App\Tweet')->create();
$this->assertNotNull($tweet->url);
}
}
Sometimes it's handy to filter through your test like so:
phpunit --filter a_tweet_url_must_not_be_null
This means that phpunit
will only execute the a_tweet_url_must_not_be_null
method. But it is recommended to run all your tests from time to time because sometime a particular test can affect other tests in your test suite.Next, let's make
a_tweet_url_must_not_be_null
test pass. We can achieve that by creating an eloquent accessor in our Tweet
model:
...
/**
* Get the tweet url
*
* @return string
*/
public function getUrlAttribute()
{
return route('user.tweet.show', ['username' => $this->user->username, 'id' => $this->id]);
}
...
Notice that we are using the Laravel's route helper function and just making up what our tweet url might look like. Come to think about it, a tweet should belongs to a user right? So, maybe it would be nice that our tweet url should look like youdomain.com/username/tweet/id
.Next, when we run our test again it should failed like so:
phpunit --filter a_tweet_url_must_not_be_null
...
1) Tests\Unit\TweetTest::a_tweet_url_must_not_be_null
ErrorException: Trying to get property of non-object
/home/vagrant/Projects/twitter/app/Tweet.php:16
...
This is because we're trying to access the username of the owner ($this->owner->username
), but we don't have an owner relationship yet in our Tweet
model.So, let's make one:
/**
* Get the owner of the tweet.
*
* @return Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function owner()
{
return $this->belongsTo(User::class, 'user_id');
}
We're using one to one relationship here because a tweet can only have one owner.Let's run our test again:
phpunit --filter a_tweet_url_must_not_be_null
...
1) Tests\Unit\TweetTest::a_tweet_url_must_not_be_null
InvalidArgumentException: Route [user.tweet.show] not defined.
...
Now, we get a different error which is a good thing because it means that the first one is already fixed, well, hopefully. 😄Next, looks like this is a route name related error. So, let's check what are available routes do we have for now, using
route:list
artisan command:
php artisan route:list
+--------+----------+----------+------+---------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+----------+----------+------+---------+--------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api,auth:api |
+--------+----------+----------+------+---------+--------------+
Notice that we don't have a route name called user.tweet.show
.Let's fix that by creating our first route. Let's add it in
routes/web.php
like so:
Route::group(['prefix' => '{username}', 'namespace' => 'User'], function () {
Route::resource('tweet', 'TweetController');
});
Notice we're using Laravel's Route Groups here to put all user related stuff in one place. I also like to use Laravel's Resource Controllers because it makes our routes file a little bit cleaner because we don't have to specify every endpoint that we need, like for example we need to show all tweets, delete tweet, edit tweet, etc. I also like the idea that the fewer methods in your controller the better.Let's run our test again:
phpunit --filter a_tweet_url_must_not_be_null
...
1) Tests\Unit\TweetTest::a_tweet_url_must_not_be_null
InvalidArgumentException: Route [user.tweet.show] not defined.
...
and we get the same error.Let's check our route list again:
php artisan route:list
[ReflectionException]
Class App\Http\Controllers\User\TweetController does not exist
Notice this time we get Reflection Exception error, and says Class App\Http\Controllers\User\TweetController
does not exist.Let's fix that by creating one using
make:controller
artisan command with --resource
flag:
php artisan make:controller User\TweetController --resource
This will create a TwitterController.php
file under app/Http/Controllers/User
directory.Next, let's check our route list again:
php artisan route:list
+--------+-----------+----------------------------+----------------+---------------------------------------------------+--------------+
| Domain | Method | URI | Name | Action | Middleware |
+--------+-----------+----------------------------+----------------+---------------------------------------------------+--------------+
| | GET|HEAD | / | | Closure | web |
| | GET|HEAD | api/user | | Closure | api,auth:api |
| | GET|HEAD | {username}/tweet | tweet.index | App\Http\Controllers\User\TweetController@index | web |
| | POST | {username}/tweet | tweet.store | App\Http\Controllers\User\TweetController@store | web |
| | GET|HEAD | {username}/tweet/create | tweet.create | App\Http\Controllers\User\TweetController@create | web |
| | GET|HEAD | {username}/tweet/{tweet} | tweet.show | App\Http\Controllers\User\TweetController@show | web |
| | PUT|PATCH | {username}/tweet/{tweet} | tweet.update | App\Http\Controllers\User\TweetController@update | web |
| | DELETE | {username}/tweet/{tweet} | tweet.destroy | App\Http\Controllers\User\TweetController@destroy | web |
| | GET|HEAD | {username}/tweet/{tweet}/edit | tweet.edit | App\Http\Controllers\User\TweetController@edit | web |
+--------+-----------+----------------------------+----------------+---------------------------------------------------+--------------+
Notice that our route names is not what we want in this case.We have two options for that, one is renaming our resource routes name manually, or update our tweet url accessor (
getUrlAttribute()
) in the Tweet
model. In this case let's do the latter:
public function getUrlAttribute()
{
return route('tweet.show', ['username' => $this->user->username, 'id' => $this->id]);
}
Next, let's die dump the tweet url property to make sure we have a correct url:
/** @test */
public function a_tweet_url_must_not_be_null()
{
$tweet = factory('App\Tweet')->create();
dd($tweet->url);
$this->assertNotNull($tweet->url);
}
Notice when we run our test the url is http://localhost/tweet/1
which is not what we want.Looks like we need another test for that. But first, if you have a custom domain in your local environment like I do, you have to change
localhost
to whatever custom domain you're using. In my case I have to change it to twitter.dev
by updating APP_URL
in my .env
file:
APP_URL=http://twitter.dev
Now, let's create a new test in our
tests/Unit/TweetTest.php
that will make sure our tweet url is in correct format:
/** @test */
public function a_tweet_url_must_in_correct_format()
{
$tweet = factory('App\Tweet')->create();
$url = url($tweet->owner->username.'/tweet/'.$tweet->id);
$this->assertEquals($url, $tweet->url);
}
Notice when we run our test again we get green, but when we die dump the tweet url (dd($tweet->url)
) it doesn't have username in it.Let's fix that by creating a unit test first for our user related stuff:
php artisan make:test UserTest --unit
/** @test */
public function a_user_must_have_a_username()
{
$user = factory('App\User')->create();
$this->assertNotNull($user->username);
}
Of course this is gonna failed.Let's make it pass that by adding a
username
column in our users
table:
...
$table->string('username');
...
Next, let's update our user model factory:
return [
'username' => $faker->word,
...
]
Next, let's run our whole test suite:
phpunit
There was 1 failure:
1) Tests\Feature\UserTest::a_user_can_view_a_tweet
Failed asserting that '' contains "Est soluta aliquam rerum.".
...
Looks like we're now closer to our goal for this lesson that a user can view a tweet. Good job!Our
a_user_can_view_a_tweet
test is failing because we don't have a view yet for our tweet. But, first let's add an authentication feature in our application using make:auth
artisan command.
php artisan make:auth
This command will generate a nice authentication scaffolding for us.Next, let's create a
show.blade.php
file under resources/views/user/tweet
directory.
@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-body">
{{ $tweet->body }}
</div>
</div>
</div>
</div>
</div>
@endsection
Next, let's update our
show
method in app/Http/Controllers/User/TweetController.php
:
/**
* Display the specified resource.
*
* @param string $username
* @param \App\Tweet $tweet
* @return \Illuminate\Http\Response
*/
public function show($username, Tweet $tweet)
{
return view('user.tweet.show', compact('tweet'));
}
Notice we're using Laravel's Implicit Route Model Binding here. We also have to accept $username
as first parameter because our route need to pass a username to the show()
method ({username}/tweet/{tweet}
).Now, when we run our test again we should get green, and when we access it in the browser using:
http://twitter.dev/maiores/tweet/1
for example, it should look like this. One more thing, make sure to run php artisan migrate:refresh --seed
first before accessing it in the browser.That's all for this lesson hope you learned something new from this lesson. If you have any questions please don't hesitate to ask in the comment below. Stay tuned for the next lesson. Thanks!
View the source code for this lesson on GitHub.
Previous: Initial Database SetupNext: Custom Authentication
Post a Comment