() 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, справляется с этой ответственностью, не имеет смысла снова тестировать эту функциональность.
Короче говоря: держите свои тесты в чистоте и организованности. Определите надлежащую ответственность каждого класса и проверяйте только то, что является его ответственностью.
Вывод

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