Статьи

Тестирование как босс в Laravel: модели

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

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


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

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

Вкратце: база данных в памяти позволяет проводить быстрые и чистые тесты.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
// app/config/testing/database.php
 
<?php
 
return array(
 
    ‘default’ => ‘sqlite’,
 
    ‘connections’ => array(
        ‘sqlite’ => array(
            ‘driver’ => ‘sqlite’,
            ‘database’ => ‘:memory:’,
            ‘prefix’ => »
        ),
    )
);

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

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

01
02
03
04
05
06
07
08
09
10
/**
 * 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);
}

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

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

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

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

01
02
03
04
05
06
07
08
09
10
/**
 * Default preparation for each test
 *
 */
public function setUp()
{
    parent::setUp();
 
    $this->prepareForTests();
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 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);
    }
}

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


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

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

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

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

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

В нашей модели Post у нас есть связь с моделью User через волшебный метод author .

Наконец, у нас есть простой метод, который возвращает дату в формате «день / месяц / год» .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 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(‘Ymd H:i:s’, $date_obj);
 
        return $date_obj->format(‘d/m/Y’);
    }
}

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

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

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

1
2
3
4
5
6
7
8
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 );
   }

Этот тест является «необязательным». Мы проверяем, что отношение « 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 также будет подготовлен и доступен.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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()) ) ?
 
        $this->assertTrue( $matches );
    }
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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()) ) ?
 
        $this->assertTrue( $matches );
    }
}

PS: я решил не писать название тестов в CamelCase для удобства чтения. PSR-1 простите меня, но testRelationWithAuthor не так удобочитаем, как я бы лично предпочел. Конечно, вы можете использовать стиль, который вам больше всего нравится.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<?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’);
    }
 
}

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?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 );
    }

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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’));
   }

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

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

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

01
02
03
04
05
06
07
08
09
10
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’));
   }

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

01
02
03
04
05
06
07
08
09
10
11
12
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’));
   }

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?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 . Вот код для этой модели:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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’ );
    }
 
}

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

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

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

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

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

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


Запуск тестов

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

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

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