If you're hoping to learn why tests are beneficial, this is not the article for you. Over the course of this tutorial, I will assume that you already understand the advantages, and are hoping to learn how best to write and organize your tests in Laravel 4.
Version 4 of Laravel offers serious improvements in relation to testing, when compared to its previous release. This is the first article of a series that will cover how to write tests for Laravel 4 applications. We'll begin the series by discussing model testing.
Setup
In-memory database
Unless you are running raw queries on your database, Laravel allows your application to remain database agnostic. With a simple driver change, your application can now work with other DBMS (MySQL, PostgreSQL, SQLite, etc.). Among the default options, SQLite offers a peculiar, yet very useful feature: in-memory databases.
With Sqlite, we can set the database connection to :memory:
, which will drastically speed up our tests, due to the database not existing on the hard disk. Moreover, the production/development database will never be populated with left-over test data, because the connection, :memory:
, always begins with an empty database.
In short: an in-memory database allows for fast and clean tests.
Within the app/config/testing
directory, create a new file, named database.php
, and fill it with the following content:
// app/config/testing/database.php <?php return array( 'default' => 'sqlite', 'connections' => array( 'sqlite' => array( 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '' ), ) );
The fact that database.php
is placed within the configuration testing
directory means that these settings will only be used when in a testing environment (which Laravel automatically sets). As such, when your application is accessed normally, the in-memory database will not be used.
Before running tests
Since the in-memory database is always empty when a connection is made, it's important to migrate the database before every test. To do this, open app/tests/TestCase.php
and add the following method to the end of the class:
/** * Migrates the database and set the mailer to 'pretend'. * This will cause the tests to run quickly. * */ private function prepareForTests() { Artisan::call('migrate'); Mail::pretend(true); }
NOTE: The
setUp()
method is executed by PHPUnit before each test.
This method will prepare the database, and change the status of Laravel's Mailer
class to pretend
. This way, the Mailer will not send any real email when running tests. Instead, it will log the "sent" messages.
To finalize app/tests/TestCase.php
, call prepareForTests()
within the PHPUnit setUp()
method, which will execute before every test.
Don't forget the
parent::setUp()
, as we're overwriting the method of the parent class.
/** * Default preparation for each test * */ public function setUp() { parent::setUp(); // Don't forget this! $this->prepareForTests(); }
At this point, app/tests/TestCase.php
should look like the following code. Remember that createApplication
is created automatically by Laravel. You don't need to worry about it.
// app/tests/TestCase.php <?php class TestCase extends Illuminate\Foundation\Testing\TestCase { /** * Default preparation for each test */ public function setUp() { parent::setUp(); $this->prepareForTests(); } /** * Creates the application. * * @return Symfony\Component\HttpKernel\HttpKernelInterface */ public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; return require __DIR__.'/../../start.php'; } /** * Migrates the database and set the mailer to 'pretend'. * This will cause the tests to run quickly. */ private function prepareForTests() { Artisan::call('migrate'); Mail::pretend(true); } }
Now, to write our tests, simply extend TestCase
, and the database will be initialized and migrated before each test.
The Tests
It is correct to say that, in this article, we won't be following the TDD process. The issue here is didactic, with the goal of demonstrating how the tests can be written. Because of this, I chose to reveal the models in question first, and then their related tests. I believe that this is a better way to illustrate this tutorial.
The context of this demo application is a simple blog/CMS, containing users (authentication), posts and static pages (which are shown in the menu).
Post model
Please note that the model extends the class, Ardent, rather than Eloquent. Ardent is a package that makes for easy validation, upon saving the model (see the $rules
property).
Next, we have the public static $factory
array, which leverages the FactoryMuff package, to assist with object creation when testing.
Both Ardentx and FactoryMuff are available through Packagist and Composer.
In our Post
model, we have a relationship with the User
model, through the magic author
method.
Finally, we have a simple method that returns the date, formatted as "day/month/year".
// app/models/Post.php <?php use LaravelBook\Ardent\Ardent; class Post extends Ardent { /** * Table */ protected $table = 'posts'; /** * Ardent validation rules */ public static $rules = array( 'title' => 'required', // Post tittle 'slug' => 'required|alpha_dash', // Post Url 'content' => 'required', // Post content (Markdown) 'author_id' => 'required|numeric', // Author id ); /** * Array used by FactoryMuff to create Test objects */ public static $factory = array( 'title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => 'factory|User', // Will be the id of an existent User. ); /** * Belongs to user */ public function author() { return $this->belongsTo( 'User', 'author_id' ); } /** * Get formatted post date * * @return string */ public function postedAt() { $date_obj = $this->created_at; if (is_string($this->created_at)) $date_obj = DateTime::createFromFormat('Y-m-d H:i:s', $date_obj); return $date_obj->format('d/m/Y'); } }
Post tests
To keep things organized, I've placed the class with the Post
model tests in app/tests/models/PostTest.php
. We'll go through all the tests, one section at a time.
// app/tests/models/PostTest.php <?php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PostTest extends TestCase {
We extend the TestCase
class, which is a requirement for PHPUnit testing in Laravel. Also, don't forget our prepareTests
method that will run before every test.
public function test_relation_with_author() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Thanks to FactoryMuff, this $post have an author $this->assertEquals( $post->author_id, $post->author->id ); }
This test is an "optional" one. We are testing that the relationship "Post
belongs to User
". The purpose here is mostly to demonstrate the functionality of FactoryMuff.
Once the Post
class have the $factory
static array containing 'author_id' => 'factory|User'
(note the source code of the model, shown above) the FactoryMuff instantiate a new User
fills its attributes, save in the database and finally return its id to the author_id
attribute in the Post
.
For this to be possible, the User
model must have a $factory
array describing its fields too.
Notice how you can access the User
relation through $post->author
. As an example, we can access the $post->author->username
, or any other existing user attribute.
The FactoryMuff package enables rapid instantiation of consistent objects for the purpose of testing, while respecting and instantiating any needed relationships. In this case, when we create a Post
with FactoryMuff::create('Post')
the User
will also be prepared and made available.
public function test_posted_at() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Regular expression that represents d/m/Y pattern $expected = '/\d{2}\/\d{2}\/\d{4}/'; // True if preg_match finds the pattern $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false; $this->assertTrue( $matches ); } }
To finish, we determine if the string returned by the postedAt()
method follows the "day/month/year" format. For such verification, a regular expression is used to test if the pattern \d{2}\/\d{2}\/\d{4}
("2 numbers" + "bar" + "2 numbers" + "bar" + "4 numbers") is found.
Alternatively, we could use PHPUnit's assertRegExp matcher.
At this point, the app/tests/models/PostTest.php
file is as follows:
// app/tests/models/PostTest.php <?php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PostTest extends TestCase { public function test_relation_with_author() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Thanks to FactoryMuff this $post have an author $this->assertEquals( $post->author_id, $post->author->id ); } public function test_posted_at() { // Instantiate, fill with values, save and return $post = FactoryMuff::create('Post'); // Regular expression that represents d/m/Y pattern $expected = '/\d{2}\/\d{2}\/\d{4}/'; // True if preg_match finds the pattern $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false; $this->assertTrue( $matches ); } }
PS: I chose not to write the name of the tests in CamelCase for readability purposes. PSR-1 forgive me, but
testRelationWithAuthor
is not as readable as I would personally prefer. You're free to use the style that you most prefer, of course.
Page model
Our CMS need a model to represent static pages. This model is implemented as follows:
<?php // app/models/Page.php use LaravelBook\Ardent\Ardent; class Page extends Ardent { /** * Table */ protected $table = 'pages'; /** * Ardent validation rules */ public static $rules = array( 'title' => 'required', // Page Title 'slug' => 'required|alpha_dash', // Slug (url) 'content' => 'required', // Content (markdown) 'author_id' => 'required|numeric', // Author id ); /** * Array used by FactoryMuff */ public static $factory = array( 'title' => 'string', 'slug' => 'string', 'content' => 'text', 'author_id' => 'factory|User', // Will be the id of an existent User. ); /** * Belongs to user */ public function author() { return $this->belongsTo( 'User', 'author_id' ); } /** * Renders the menu using cache * * @return string Html for page links. */ public static function renderMenu() { $pages = Cache::rememberForever('pages_for_menu', function() { return Page::select(array('title','slug'))->get()->toArray(); }); $result = ''; foreach( $pages as $page ) { $result .= HTML::action( 'PagesController@show', $page['title'], ['slug'=>$page['slug']] ).' | '; } return $result; } /** * Forget cache when saved */ public function afterSave( $success ) { if( $success ) Cache::forget('pages_for_menu'); } /** * Forget cache when deleted */ public function delete() { parent::delete(); Cache::forget('pages_for_menu'); } }
We can observe that the static method, renderMenu()
, renders a number of links for all existing pages. This value is saved in the cache key, 'pages_for_menu'
. This way, in future calls to renderMenu()
, there will be no need to hit the real database. This can provide significant improvements to our application's performance.
However, if a Page
is saved or deleted (afterSave()
and delete()
methods), the value of the cache will be cleared, causing the renderMenu()
to reflect the new state of database. So, if the name of a page is changed, or if it is deleted, the key 'pages_for_menu'
is cleared from the cache. (Cache::forget('pages_for_menu');
)
NOTE: The method,
afterSave()
, is available through the Ardent package. Otherwise, it would be necessary to implement thesave()
method to clean the cache and callparent::save()
;
Page tests
In: app/tests/models/PageTest.php
, we'll write the following tests:
<?php // app/tests/models/PageTest.php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PageTest extends TestCase { public function test_get_author() { $page = FactoryMuff::create('Page'); $this->assertEquals( $page->author_id, $page->author->id ); }
Once again, we have an "optional" test to confirm the relationship. As relationships are the responsibility of Illuminate\Database\Eloquent
, which is already covered by Laravel's own tests, we don't need to write another test to confirm that this code works as expected.
public function test_render_menu() { $pages = array(); for ($i=0; $i < 4; $i++) { $pages[] = FactoryMuff::create('Page'); } $result = Page::renderMenu(); foreach ($pages as $page) { // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan(0, strpos($result, $page->slug)); } // Check if cache has been written $this->assertNotNull(Cache::get('pages_for_menu')); }
This is one of the most important tests for the Page
model. First, four pages are created in the for
loop. Following that, the result of the renderMenu()
call is stored in the $result
variable. This variable should contain an HTML string, containing links to the existing pages.
The foreach
loop checks if the slug (url) of each page is present in $result
. This is enough, since the exact format of the HTML is not relevant to our needs.
Finally, we determine if the cache key, pages_for_menu
, has something stored. In other words, did the renderMenu()
call actually saved some value to the cache?
public function test_clear_cache_after_save() { // An test value is saved in cache Cache::put('pages_for_menu','avalue', 5); // This should clean the value in cache $page = FactoryMuff::create('Page'); $this->assertNull(Cache::get('pages_for_menu')); }
This test aims to verify if, when saving a new Page
, the cache key 'pages_for_menu'
is emptied. The FactoryMuff::create('Page');
eventually triggers the save()
method, so that should suffice for the key, 'pages_for_menu'
, to be cleared.
public function test_clear_cache_after_delete() { $page = FactoryMuff::create('Page'); // An test value is saved in cache Cache::put('pages_for_menu','value', 5); // This should clean the value in cache $page->delete(); $this->assertNull(Cache::get('pages_for_menu')); }
Similar to the previous test, this one determines if the key 'pages_for_menu'
is emptied properly after deleting a Page
.
Your PageTest.php
should look like so:
<?php // app/tests/models/PageTest.php use Zizaco\FactoryMuff\Facade\FactoryMuff; class PageTest extends TestCase { public function test_get_author() { $page = FactoryMuff::create('Page'); $this->assertEquals( $page->author_id, $page->author->id ); } public function test_render_menu() { $pages = array(); for ($i=0; $i < 4; $i++) { $pages[] = FactoryMuff::create('Page'); } $result = Page::renderMenu(); foreach ($pages as $page) { // Check if each page slug(url) is present in the menu rendered. $this->assertGreaterThan(0, strpos($result, $page->slug)); } // Check if cache has been written $this->assertNotNull(Cache::get('pages_for_menu')); } public function test_clear_cache_after_save() { // An test value is saved in cache Cache::put('pages_for_menu','avalue', 5); // This should clean the value in cache $page = FactoryMuff::create('Page'); $this->assertNull(Cache::get('pages_for_menu')); } public function test_clear_cache_after_delete() { $page = FactoryMuff::create('Page'); // An test value is saved in cache Cache::put('pages_for_menu','value', 5); // This should clean the value in cache $page->delete(); $this->assertNull(Cache::get('pages_for_menu')); } }
User model
Related to the previously presented models, we now have the User
. Here's the code for that model:
<?php // app/models/User.php use Zizaco\Confide\ConfideUser; class User extends ConfideUser { // Array used in FactoryMuff public static $factory = array( 'username' => 'string', 'email' => 'email', 'password' => '123123', 'password_confirmation' => '123123', ); /** * Has many pages */ public function pages() { return $this->hasMany( 'Page', 'author_id' ); } /** * Has many posts */ public function posts() { return $this->hasMany( 'Post', 'author_id' ); } }
This model is absent of tests.
We can observe that, with the exception of relationships (which can be helpful to test), there is not any method implementation here. What about authentication? Well, the use of the Confide package already provides the implementation and tests for this.
The tests for
Zizaco\Confide\ConfideUser
are located in ConfideUserTest.php.
It's important to determine class responsibilities before writing your tests. Testing the option to "reset the password" of a User
would be redundant. This is because the proper responsibility for this test is within Zizaco\Confide\ConfideUser
; not in User
.
The same is true for data validation tests. As the package, Ardent, handles this responsibility, it wouldn't make much sense to test the functionality again.
In Short: keep your tests clean and organized. Determine the proper responsibility of each class, and test only what is strictly its responsibility.
Conclusion
The use of an in-memory database is a good practice to execute tests against a database quickly. Thanks to help from some packages, such as Ardent, FactoryMuff and Confide, you can minimize the amount of code in your models, while keeping the tests clean and objective.
In the sequel to this article, we'll review Controller testing. Stay tuned!
Still getting started with Laravel 4, let us teach you the essentials!
Comments