Когда вы пишете тесты для модели Eloquent, зачастую нужен доступ непосредственно к базе данных, что бы проверить свой код в боевых условиях.
Но иногда функциональность тестирования не зависит от особенностей базы данных. Есть ли способ тестировать данные без отправки в базу данных?
Тестирование с базой данных
Скажем так, у нас есть альбомы Albums
с песнями Songs
и мы хотели бы протестировать расчёт общей продолжительность каждого альбома.
Тест может выглядеть следующим образом:
class AlbumTest extends TestCase
{
use DatabaseMigrations;
public function test_can_calculate_total_duration()
{
// 1. Создаём и сохраняем альбом в базе данных
$album = factory('Album')->create();
// 2. Создаем несколько песен с установленной продолжительностью
$songs = new Collection([
factory('Song')->make(['duration' => 291]),
factory('Song')->make(['duration' => 123]),
factory('Song')->make(['duration' => 100]),
]);
// 3. Сохраняем их через отношение `songs`
// для установки внешних ключей
$album->songs()->saveMany($songs);
// 4. Убеждаемся, что продолжительность альбома // равна продолжительности всех песен
$this->assertEquals(514, $album->duration);
}
}
Простейшая реализация $album->duration
может выглядеть следующим образом:
class Album extends Model
{
// ...
public function getDurationAttribute()
{
return $this->songs->sum('duration');
}
// ...
}
Если мы запустим этот тест то всё пройдет гладко, на моей машине он занял примерно ~ 90 мс.
Этот вариант подходит для тестов, которым действительно необходимо соединение с базой данных, и я полностью с этим согласен.
Но в данном случае мы не пытаемся проверить определенную область запроса или убедиться, что данные успешно сохранены, мы просто хотим посчитать некоторое число!
Для этого должен существовать другой путь!
In-memory отношения
Когда вы выполняете “нетерпеливую загрузку” (eager load) отношения, использование метода $album->songs
не выполняет новый запрос; вместо этого происходит извлечение сохранённого в памяти Albums
свойства Songs
.
Можно ссылаться на нетерпеливого загрузку отношения вручную с помощью метода setRelation
:
public function test_can_calculate_total_duration()
{
$album = factory('Album')->create();
$songs = new Collection([
factory('Song')->make(['duration' => 291]),
factory('Song')->make(['duration' => 123]),
factory('Song')->make(['duration' => 100]),
]);
- $album->songs()->saveMany($songs);
+ $album->setRelation('songs', $songs);
$this->assertEquals(514, $album->duration);
}
Теперь мы можем избавиться от трейта DatabaseMigrations
и использовать make
для создания Album
вместо create
, а оставшийся тест будет выглядеть следующим образом:
class AlbumTest extends TestCase
{
public function test_can_calculate_total_duration()
{
$album = factory('Album')->make();
$songs = new Collection([
factory('Song')->make(['duration' => 291]),
factory('Song')->make(['duration' => 123]),
factory('Song')->make(['duration' => 100]),
]);
$album->setRelation('songs', $songs);
$this->assertEquals(514, $album->duration);
}
}
Выполнение этого теста все еще занимает некоторое время, но на этот раз лишь 3мс! Это в тридцать раз быстрее.
Ничто не бесплатно
У всего есть минусы, и данный подход не исключение.
Если мы изменили нашу реализацию, чтобы суммировать продолжительность песен в базе данных, наш новый тест потерпит неудачу:
class Album extends Model
{
// ...
public function getDurationAttribute()
{
// Обратите внимание на дополнительные скобки!
return $this->songs()->sum('duration');
}
// ...
}
Таким образом, в то время как повышение производительности колоссально, этот подход подводит нас к определенному виду реализации, который менее гибок для рефакторинга.
В любом случае, это крутой трюк, который стоит иметь в запасе, и если ваше окружение для тестирования работает медленно, плюсы данного подхода покрывают все минусы.