Advertisement
  1. Code
  2. PHP
  3. Laravel

Тестирование в Laravel: Модели

Scroll to top
Read Time: 13 min

() translation by (you can also view the original English article)

Если вы надеетесь узнать, почему тесты полезны, это не статья для вас. В ходе этого урока я предполагаю, что вы уже понимаете преимущества и надеетесь узнать, как лучше писать и организовывать свои тесты в Laravel 4.

Версия 4 Laravel предлагает серьезные улучшения в отношении тестирования по сравнению с предыдущим релизом. Это первая статья серии, в которой рассказывается, как писать тесты для приложений Laravel 4. Мы начнем серию, обсуждая тестирование модели.


Настройка

База данных в памяти

Если вы не используете сырые запросы в своей базе данных, Laravel позволяет вашему приложению оставаться агностиком базы данных. При простом изменении драйвера ваше приложение теперь может работать с другими СУБД (MySQL, PostgreSQL, SQLite и т.д.). Среди стандартных опций SQLite предлагает своеобразную, но очень полезную функцию: базы данных в памяти.

С Sqlite мы можем установить подключение к базе данных :memory:, что значительно ускорит наши тесты так как база данных не будет располагаться на жестком диске. Кроме того, база данных никогда не будет заполнена данными, оставшимися от предыдущих тестов, потому что соединение :memory: всегда начинается с пустой базы данных.

Короче говоря: база данных в памяти позволяет быстро и чисто тестировать приложение.

В каталоге app/config/testing создайте новый файл с именем database.php и заполните его следующим содержимым:

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
);

Тот факт, что database.php помещен в каталог конфигурации testing, означает, что эти параметры будут использоваться только в тестовой среде (которую автоматически устанавливает Laravel). Таким образом, когда к вашему приложению обращаются нормально, база данных в памяти не будет использоваться.

Перед запуском тестов

Поскольку база данных в памяти всегда пуста при подключении, важно migrate базу данных перед каждым тестом. Для этого откройте app/tests/TestCase.php и добавьте следующий метод в конец класса:

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
}

ПРИМЕЧАНИЕ. Метод setUp() выполняется PHPUnit перед каждым тестом.

Этот метод подготовит базу данных и изменит статус класса Mailer Laravel на pretend. Таким образом, Mailer не будет отправлять какие-либо реальные сообщения при запуске тестов. Вместо этого он будет записывать «отправленные» сообщения.

Чтобы завершить app/tests/TestCase.php, вызовите метод prepareForTests() в методе setUp() PHPUnit, который будет выполняться перед каждым тестом.

Не забудьте вызвать parent::setUp(), поскольку мы переписываем метод родительского класса.

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
}

На этом этапе app/tests/TestCase.php должен выглядеть следующим образом. Помните, что createApplication создается автоматически Laravel. Вам не нужно беспокоиться об этом.

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
}

Теперь, чтобы написать наши тесты, просто наследуйтесь от TestCase, и база данных будет инициализирована и мигрирована перед каждым тестом.


Тесты

Правильно сказать, что в этой статье мы не будем следить за процессом TDD. Проблема здесь дидактична, с целью продемонстрировать, как тесты могут быть написаны. Из-за этого я решил сначала выявить модели, о которых идет речь, а затем связанные с ними тесты. Я считаю, что это лучший способ проиллюстрировать этот урок.

Контекст этого демонстрационного приложения - это простой блог/CMS, содержащий пользователей (аутентификация), сообщения и статические страницы (которые показаны в меню).

Модель Post

Обратите внимание, что модель расширяет класс, Ardent, а не Eloquent. Ardent - это пакет, который упрощает проверку при сохранении модели (см. свойство $rules).

Затем у нас есть public static $factory массив, который использует пакет FactoryMuff, чтобы помочь при создании объекта для тестирования.

Как Ardentx, так и FactoryMuff доступны через Packagist и Composer.

В нашей модели Post мы имеем отношение к модели User, используя магический метод author.

Наконец, у нас есть простой метод, который возвращает дату, отформатированную как «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

Чтобы все было организовано, я поместил класс с тестами модели Post в app/tests/models/PostTest.php. Мы пройдем все тесты, по одной секции за раз.

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

Мы расширяем класс TestCase, что является требованием для тестирования PHPUnit в Laravel. Кроме того, не забывайте о нашем методе prepareTests, который будет выполняться перед каждым тестом.

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
    }

Этот тест является «необязательным». Мы проверяем, что отношение «Post принадлежит User». Цель здесь - в основном продемонстрировать функциональность FactoryMuff.

Когда класс Post имеет статический массив $factory, содержащий 'author_id' => 'factory|User' (обратите внимание на исходный код модели, показанный выше), FactoryMuff создает экземпляр нового User, который заполняет его атрибуты, сохраняет в базе данных и, наконец, возвращает его author_id атрибут в Post.

Чтобы это было возможно, модель User должна иметь массив $factory, описывающий его поля.

Обратите внимание, как вы можете получить доступ к отношениям User через $post->author. Например, мы можем получить доступ к $post->author->username или любому другому существующему пользовательскому атрибуту.

Пакет FactoryMuff позволяет быстро создавать последовательные объекты для целей тестирования, соблюдая и создавая любые необходимые отношения. В этом случае, когда мы создаем Post с FactoryMuff::create('Post'), User также будет подготовлен и доступен.

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
}

Чтобы закончить, мы определяем, следует ли строка, возвращаемая методом publishAt(),  формату «день/месяц/год». Для такой проверки мы используем регулярное выражение, если шаблон \d{2}\/\d{2}\/\d{4}("2 числа" + "bar" + "2 цифры" + "bar «+» 4 числа»).

В качестве альтернативы мы могли бы использовать assertRegExp PHPUnit.

На этом этапе файл app/tests/models/PostTest.php выглядит следующим образом:

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: Я решил не писать названия тестов в CamelCase для удобства чтения. PSR-1 простите меня, но testRelationWithAuthor не так читается, как я бы предпочел лично. Конечно, вы можете использовать стиль, который вам больше всего нравится.

Модель Page

Нашей CMS нужна модель для представления статических страниц. Эта модель реализована следующим образом:

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
}

Мы можем заметить, что статический метод renderMenu() предоставляет ряд ссылок для всех существующих страниц. Это значение сохраняется в кеше под ключом  'pages_for_menu'. Таким образом, в будущем вызовы renderMenu() не будут нуждаться в реальной базе данных. Это может значительно улучшить производительность нашего приложения.

Однако, если Page сохранена или удалена (методы afterSave() и delete()), значение кеша будет очищено, в результате renderMenu() будет отражать новое состояние базы данных. Итак, если имя страницы изменено или если оно удалено, ключ «pages_for_menu» очищается из кеша. (Cache::forget( 'pages_for_menu');)

ПРИМЕЧАНИЕ. Метод afterSave() доступен через пакет Ardent. В противном случае было бы необходимо реализовать метод save() для очистки кеша и вызвать parent::save();

Тестирование Page

В: app/tests/models/PageTest.php, мы напишем следующие тесты:

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
    }

Еще раз, у нас есть «необязательный» тест, подтверждающий отношения. Поскольку отношениями являются Illuminate\Database\Eloquent, которые уже охвачены собственными тестами Laravel, нам не нужно писать еще один тест, чтобы подтвердить, что этот код работает так, как ожидалось.

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
    }

Это один из самых важных тестов для модели Page. Сначала в цикле for создаются четыре страницы. После этого результат вызова renderMenu() сохраняется в переменной $result. Эта переменная должна содержать строку HTML, содержащую ссылки на существующие страницы.

Цикл foreach проверяет, присутствует ли слаг (url) каждой страницы в $result. Этого достаточно, поскольку точный формат HTML не имеет отношения к нашим потребностям.

Наконец, мы определяем, хранит ли кэш какие-нибудь данные по ключу pages_for_menu. Другими словами, действительно ли вызов renderMenu() фактически сохранил некоторое значение в кеше?

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
    }

Этот тест предназначен для проверки того, сохраняется ли при сохранении новой Page ключ кеша 'pages_for_menu'. FactoryMuff::create('Page'); в конечном итоге вызывает метод save(), поэтому для ключа должно быть достаточно pages_for_menu, которое должно быть очищено.

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
    }

Как и в предыдущем тесте, это определяет, правильно ли освобождается ключ 'pages_for_menu' после удаления Page.

Ваш PageTest.php должен выглядеть так:

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

В связи с ранее представленными моделями у нас теперь есть User. Вот код для этой модели:

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
}

Эта модель отсутствует в тестах.

Мы можем заметить, что, за исключением отношений (которые могут быть полезны для тестирования), здесь нет никакой реализации. Как насчет аутентификации? Ну, использование пакета Confide уже обеспечивает реализацию и тесты для этого.

Тесты для Zizaco\Confide\ConfideUser расположены в ConfideUserTest.php.

Перед написанием тестов важно определить обязанности класса. Тестирование опции «сброса пароля» User будет избыточным. Это связано с тем, что надлежащая ответственность за этот тест принадлежит Zizaco\Confide\ConfideUser; а не  User.

То же самое верно для тестов проверки данных. Поскольку пакет, Ardent, справляется с этой ответственностью, не имеет смысла снова тестировать эту функциональность.

Короче говоря: держите свои тесты в чистоте и организованности. Определите надлежащую ответственность каждого класса и проверяйте только то, что является его ответственностью.


Вывод

Running Tests

Использование базы данных в памяти - хорошая практика для быстрого выполнения тестов с базой данных. Благодаря помощи некоторых пакетов, таких как Ardent, FactoryMuff и Confide, вы можете минимизировать количество кода в своих моделях, сохраняя при этом тесты чистыми и объективными.

В продолжении этой статьи мы рассмотрим тестирование контроллера. Будьте на связи!

Все еще новичок в Laravel 4, давайте научим вас основам!

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.