Если вы надеетесь узнать, почему тесты полезны , эта статья не для вас. В ходе этого урока я буду предполагать, что вы уже понимаете преимущества и надеетесь узнать, как лучше всего написать и организовать свои тесты в 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 , давайте научим вас основам!