Заглушки Eloquent-отношений для быстрых тестов

Когда вы пишете тесты для модели 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');
    }
    // ...
}

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

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