Advertisement
  1. Code
  2. PHP
  3. Laravel

Testing Like a Boss in Laravel: Models

Scroll to top

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:

1
// app/config/testing/database.php
2
3
<?php
4
5
return array(
6
7
    'default' => 'sqlite',
8
9
    'connections' => array(
10
        'sqlite' => array(
11
            'driver'   => 'sqlite',
12
            'database' => ':memory:',
13
            'prefix'   => ''
14
        ),
15
    )
16
);

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:

1
/**

2
 * Migrates the database and set the mailer to 'pretend'.

3
 * This will cause the tests to run quickly.

4
 *

5
 */
6
private function prepareForTests()
7
{
8
    Artisan::call('migrate');
9
    Mail::pretend(true);
10
}

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.

1
/**

2
 * Default preparation for each test

3
 *

4
 */
5
public function setUp()
6
{
7
    parent::setUp(); // Don't forget this!

8
9
    $this->prepareForTests();
10
}

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.

1
// app/tests/TestCase.php
2
3
<?php
4
5
class TestCase extends Illuminate\Foundation\Testing\TestCase {
6
7
    /**

8
     * Default preparation for each test

9
     */
10
    public function setUp()
11
    {
12
        parent::setUp();
13
14
        $this->prepareForTests();
15
    }
16
17
    /**

18
     * Creates the application.

19
     *

20
     * @return Symfony\Component\HttpKernel\HttpKernelInterface

21
     */
22
    public function createApplication()
23
    {
24
        $unitTesting = true;
25
26
        $testEnvironment = 'testing';
27
28
        return require __DIR__.'/../../start.php';
29
    }
30
31
    /**

32
     * Migrates the database and set the mailer to 'pretend'.

33
     * This will cause the tests to run quickly.

34
     */
35
    private function prepareForTests()
36
    {
37
        Artisan::call('migrate');
38
        Mail::pretend(true);
39
    }
40
}

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".

1
// app/models/Post.php
2
3
<?php
4
5
use LaravelBook\Ardent\Ardent;
6
7
class Post extends Ardent {
8
9
    /**

10
     * Table

11
     */
12
    protected $table = 'posts';
13
14
    /**

15
     * Ardent validation rules

16
     */
17
    public static $rules = array(
18
        'title' => 'required',              // Post tittle

19
        'slug' => 'required|alpha_dash',    // Post Url

20
        'content' => 'required',            // Post content (Markdown)

21
        'author_id' => 'required|numeric',  // Author id

22
    );
23
24
    /**

25
     * Array used by FactoryMuff to create Test objects

26
     */
27
    public static $factory = array(
28
        'title' => 'string',
29
        'slug' => 'string',
30
        'content' => 'text',
31
        'author_id' => 'factory|User', // Will be the id of an existent User.

32
    );
33
34
    /**

35
     * Belongs to user

36
     */
37
    public function author()
38
    {
39
        return $this->belongsTo( 'User', 'author_id' );
40
    }
41
42
    /**

43
     * Get formatted post date

44
     *

45
     * @return string

46
     */
47
    public function postedAt()
48
    {
49
        $date_obj =  $this->created_at;
50
51
        if (is_string($this->created_at))
52
            $date_obj =  DateTime::createFromFormat('Y-m-d H:i:s', $date_obj);
53
54
        return $date_obj->format('d/m/Y');
55
    }
56
}

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.

1
// app/tests/models/PostTest.php
2
3
<?php
4
5
use Zizaco\FactoryMuff\Facade\FactoryMuff;
6
7
class PostTest extends TestCase
8
{

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.

1
    public function test_relation_with_author()
2
    {
3
        // Instantiate, fill with values, save and return

4
        $post = FactoryMuff::create('Post');
5
6
        // Thanks to FactoryMuff, this $post have an author

7
        $this->assertEquals( $post->author_id, $post->author->id );
8
    }

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.

1
    public function test_posted_at()
2
    {
3
        // Instantiate, fill with values, save and return

4
        $post = FactoryMuff::create('Post');
5
6
        // Regular expression that represents d/m/Y pattern

7
        $expected = '/\d{2}\/\d{2}\/\d{4}/';
8
9
        // True if preg_match finds the pattern

10
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;
11
12
        $this->assertTrue( $matches );
13
    }
14
}

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:

1
// app/tests/models/PostTest.php
2
3
<?php
4
5
use Zizaco\FactoryMuff\Facade\FactoryMuff;
6
7
class PostTest extends TestCase
8
{
9
    public function test_relation_with_author()
10
    {
11
        // Instantiate, fill with values, save and return

12
        $post = FactoryMuff::create('Post');
13
14
        // Thanks to FactoryMuff this $post have an author

15
        $this->assertEquals( $post->author_id, $post->author->id );
16
    }
17
18
    public function test_posted_at()
19
    {
20
        // Instantiate, fill with values, save and return

21
        $post = FactoryMuff::create('Post');
22
23
        // Regular expression that represents d/m/Y pattern

24
        $expected = '/\d{2}\/\d{2}\/\d{4}/';
25
26
        // True if preg_match finds the pattern

27
        $matches = ( preg_match($expected, $post->postedAt()) ) ? true : false;
28
29
        $this->assertTrue( $matches );
30
    }
31
}

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:

1
<?php
2
3
// app/models/Page.php

4
5
use LaravelBook\Ardent\Ardent;
6
7
class Page extends Ardent {
8
9
    /**

10
     * Table

11
     */
12
    protected $table = 'pages';
13
14
    /**

15
     * Ardent validation rules

16
     */
17
    public static $rules = array(
18
        'title' => 'required',              // Page Title

19
        'slug' => 'required|alpha_dash',    // Slug (url)

20
        'content' => 'required',            // Content (markdown)

21
        'author_id' => 'required|numeric',  // Author id

22
    );
23
24
    /**

25
     * Array used by FactoryMuff

26
     */
27
    public static $factory = array(
28
        'title' => 'string',
29
        'slug' => 'string',
30
        'content' => 'text',
31
        'author_id' => 'factory|User',  // Will be the id of an existent User.

32
    );
33
34
    /**

35
     * Belongs to user

36
     */
37
    public function author()
38
    {
39
        return $this->belongsTo( 'User', 'author_id' );
40
    }
41
42
    /**

43
     * Renders the menu using cache

44
     *

45
     * @return string Html for page links.

46
     */
47
    public static function renderMenu()
48
    {
49
        $pages = Cache::rememberForever('pages_for_menu', function()
50
        {
51
            return Page::select(array('title','slug'))->get()->toArray();
52
        });
53
54
        $result = '';
55
56
        foreach( $pages as $page )
57
        {
58
            $result .= HTML::action( 'PagesController@show', $page['title'], ['slug'=>$page['slug']] ).' | ';
59
        }
60
61
        return $result;
62
    }
63
64
    /**

65
     * Forget cache when saved

66
     */
67
    public function afterSave( $success )
68
    {
69
        if( $success )
70
            Cache::forget('pages_for_menu');
71
    }
72
73
    /**

74
     * Forget cache when deleted

75
     */
76
    public function delete()
77
    {
78
        parent::delete();
79
        Cache::forget('pages_for_menu');
80
    }
81
82
}

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 the save() method to clean the cache and call parent::save();

Page tests

In: app/tests/models/PageTest.php, we'll write the following tests:

1
<?php
2
3
// app/tests/models/PageTest.php

4
5
use Zizaco\FactoryMuff\Facade\FactoryMuff;
6
7
class PageTest extends TestCase
8
{
9
    public function test_get_author()
10
    {
11
        $page = FactoryMuff::create('Page');
12
13
        $this->assertEquals( $page->author_id, $page->author->id );
14
    }

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.

1
    public function test_render_menu()
2
    {
3
        $pages = array();
4
5
        for ($i=0; $i < 4; $i++) {
6
            $pages[] = FactoryMuff::create('Page');
7
        }
8
9
        $result = Page::renderMenu();
10
11
        foreach ($pages as $page)
12
        {
13
            // Check if each page slug(url) is present in the menu rendered.

14
            $this->assertGreaterThan(0, strpos($result, $page->slug));
15
        }
16
17
        // Check if cache has been written

18
        $this->assertNotNull(Cache::get('pages_for_menu'));
19
    }

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?

1
    public function test_clear_cache_after_save()
2
    {
3
        // An test value is saved in cache

4
        Cache::put('pages_for_menu','avalue', 5);
5
6
        // This should clean the value in cache

7
        $page = FactoryMuff::create('Page');
8
9
        $this->assertNull(Cache::get('pages_for_menu'));
10
    }

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.

1
    public function test_clear_cache_after_delete()
2
    {
3
        $page = FactoryMuff::create('Page');
4
5
        // An test value is saved in cache

6
        Cache::put('pages_for_menu','value', 5);
7
8
        // This should clean the value in cache

9
        $page->delete();
10
11
        $this->assertNull(Cache::get('pages_for_menu'));
12
    }

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:

1
<?php
2
3
// app/tests/models/PageTest.php

4
5
use Zizaco\FactoryMuff\Facade\FactoryMuff;
6
7
class PageTest extends TestCase
8
{
9
    public function test_get_author()
10
    {
11
        $page = FactoryMuff::create('Page');
12
13
        $this->assertEquals( $page->author_id, $page->author->id );
14
    }
15
16
    public function test_render_menu()
17
    {
18
        $pages = array();
19
20
        for ($i=0; $i < 4; $i++) {
21
            $pages[] = FactoryMuff::create('Page');
22
        }
23
24
        $result = Page::renderMenu();
25
26
        foreach ($pages as $page)
27
        {
28
            // Check if each page slug(url) is present in the menu rendered.

29
            $this->assertGreaterThan(0, strpos($result, $page->slug));
30
        }
31
32
        // Check if cache has been written

33
        $this->assertNotNull(Cache::get('pages_for_menu'));
34
    }
35
36
    public function test_clear_cache_after_save()
37
    {
38
        // An test value is saved in cache

39
        Cache::put('pages_for_menu','avalue', 5);
40
41
        // This should clean the value in cache

42
        $page = FactoryMuff::create('Page');
43
44
        $this->assertNull(Cache::get('pages_for_menu'));
45
    }
46
47
    public function test_clear_cache_after_delete()
48
    {
49
        $page = FactoryMuff::create('Page');
50
51
        // An test value is saved in cache

52
        Cache::put('pages_for_menu','value', 5);
53
54
        // This should clean the value in cache

55
        $page->delete();
56
57
        $this->assertNull(Cache::get('pages_for_menu'));
58
    }
59
}

User model

Related to the previously presented models, we now have the User. Here's the code for that model:

1
<?php
2
3
// app/models/User.php

4
5
use Zizaco\Confide\ConfideUser;
6
7
class User extends ConfideUser {
8
9
    // Array used in FactoryMuff

10
    public static $factory = array(
11
        'username' => 'string',
12
        'email' => 'email',
13
        'password' => '123123',
14
        'password_confirmation' => '123123',
15
    );
16
17
    /**

18
     * Has many pages

19
     */
20
    public function pages()
21
    {
22
        return $this->hasMany( 'Page', 'author_id' );
23
    }
24
25
    /**

26
     * Has many posts

27
     */
28
    public function posts()
29
    {
30
        return $this->hasMany( 'Post', 'author_id' );
31
    }
32
33
}

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

Running Tests

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!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.