Как сделать тестирование приложений проще и читабельнее

Автоматизированные тесты служат нескольким целям. Тесты гарантируют, что код по-прежнему работает так, как вы ожидаете, после внесения изменений. Но они также служат другой цели, которую, как мне кажется, часто упускают из виду: они являются документацией. Таким образом, тесты должны быть не только просты в написании и выполнении, но и легко читаемы.

Тесты как документация

Документация имеет раздражающее свойство устаревать, как только вы нажимаете кнопку Сохранить. Я думаю, что большинство из нас находили в коде комментарий, который говорил одно, но код делал что-то другое. Мне любопытно, сколько часов/дней/месяцев разработчика было потеряно из-за неправильной документации. Держу пари, это очень много.

На мой взгляд, тесты – это окончательная документация, которая доказуемо правильна (или неверна). Я обнаружил, что лучший способ ознакомиться с кодом или его частью – это прочитать его тесты. Если набор тестов хорошо написан (а я понимаю, что это довольно большое “если”), они служат в качестве исполняемой спецификации.

Что делает тест “читабельным”?

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

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

Давайте рассмотрим пример. Обратите внимание, что я использую некоторые хелперы Laravel, но эта концепция применима к любому виду теста.

class GameTest extends TestCase
{
    /** @test */
    public function creatingANewGame(): void
    {
        $user = User::factory()->create();

        $this->actingAs($user)->post(route('games.store'), [
          'name' => 'My super cool game',
          'is_public' => 1,
          'description' => 'A really long description that no one will ever read.',
        ]);

        $this->assertDatabaseHas('games', [
          'name' => 'My super cool game',
          'is_public' => 1,
          'description' => 'A really long description that no one will ever read.',
        ]);
    }
}

В этом примере мы тестируем, что можем создать новую игру, отправив POST-запрос в определенный эндпоинт. Затем, чтобы утверждать, что это сработало, мы проверяем, существует ли запись в правильной таблице с правильными атрибутами.

Пока все хорошо. Давайте рассмотрим другой пример. Вот тест для объекта value, который представляет собой какой-то серийный номер.

class SerialNumberTest extends TestCase
{
  /** @test */
  public function canBeConstructedWithAValidSerialNumber(): void
  {
    $serialNumber = SerialNumber::fromString('ABCDEFG10234');

    $this->assertEquals('ABCDEFG10234', (string) $serialNumber);
  }

  /** @test */
  public function throwsAnExceptionIfSerialNumberIsInvalid(): void
  {
    $this->expectException(InvalidArgumentException::class);

    SerialNumber::fromString('9999999999');
  }

  // ... more test cases
}

Эти тесты очень разные. Один тестирует всю конечную точку, в то время как другой тестирует один объект value. 

Хотелось бы обратить внимание на строки, которые мы передаем тестируемым методам. Они совершенно разные и не несут никакого смысла. Они несут совершенно иной смысл в наших тестах.

Значение строки

Давайте еще раз посмотрим на первый тест.

/** @test */
public function creatingANewGame(): void
{
    $user = User::factory()->create();

    $this->actingAs($user)->post(route('games.store'), [
        'name' => 'My super cool game',
        'is_public' => 1,
        'description' => 'A really long description that no one will ever read.',
    ]);

    $this->assertDatabaseHas('games', [
        'name' => 'My super cool game',
        'is_public' => 1,
        'description' => 'A really long description that no one will ever read.',
    ]);
}

Имеет ли значение то, что наша игра называется ‘My super cool game’? Конечно нет. Все что нас волнует, так это то, что название игры переданное в эндпоинт должно равняться значению в базе данных.

Теперь давайте еще раз посмотрим на наш тест value object:

/** @test */
public function canBeConstructedWithAValidSerialNumber(): void
{
    $serialNumber = SerialNumber::fromString('ABCDEFG10234');

    $this->assertEquals('ABCDEFG10234', (string) $serialNumber);
}

/** @test */
public function throwsAnExceptionIfSerialNumberIsInvalid(): void
{
    $this->expectException(InvalidArgumentException::class);

    SerialNumber::fromString('9999999999');
}

Часть ответственности value object заключается в обеспечении того, чтобы он всегда представлял собой действительный серийный номер. Он сообщает читателю, что точное значение ABCDEFG10234 является действительным серийным номером, тогда как 9999999999 это не так.

Сделайте очевидным, что важно, а что нет

Мне хотелось бы поделиться техникой, которую я узнал из курса J.B. Rainsberger  The World’s best intro to TDD

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

/** @test */
public function creatingANewGame(): void
{
    $user = User::factory()->create();

    $this->actingAs($user)->post(route('games.store'), [
        'name' => '::name::',
        'is_public' => 1,
        'description' => '::description::',
    ]);

    $this->assertDatabaseHas('games', [
        'name' => '::name::',
        'is_public' => 1,
        'description' => '::description::',
    ]);
}

Это говорит о том, что ::name::и ::description::представляют собой название и описание игры, соответственно. Но не важно, каковы их реальные значения. Они могут быть любыми, если они соответствуют тому, что сохраняется в базе данных.

Затем, всякий раз, когда вы видите тест, в котором я не использую этот ::syntax::, вы будете знать, что конкретное значение этой строки имеет значение здесь. Как в Примере с серийным номером.

/** @test */
public function canBeConstructedWithAValidSerialNumber(): void
{
    // This can't just be any string. The value matters.
    $serialNumber = SerialNumber::fromString('ABCDEFG10234');

    $this->assertEquals('ABCDEFG10234', (string) $serialNumber);
}

Вывод

Тесты – это документация. Всякий раз, когда мы пишем тесты, мы должны убедиться, что убрали как можно больше двусмысленности. Используя двойное двоеточие ::syntax::, мы можем устранить возможный источник путаницы. Я обнаружил, что это действительно ценное изменение в том, как я пишу свои тесты. Теперь я могу с первого взгляда определить, какую роль играет строка в тесте.

Оригинал https://www.kai-sassnowski.com/post/reducing-unnecessary-details-in-tests/

Перевод Максим Гречушников

Рейтинг
( 1 оценка, среднее 4 из 5 )
Maxyc Webber/ автор статьи
Мне 35 лет. Опыт профессиональной разработки 15 лет. Занимаюсь разработкой и поддержкой корпоративных систем автоматизации бизнеса, а также высоконагруженными проектами. Мне нравится решать нестандартные проблемы бизнеса. Имею опыт формирования команд под проект, налаживания процесса разработки, коммуникации программистов и заказчиков. Есть опыт работы с зарубежными заказчиками (ОАЭ, Польша, Германия, Швейцария).
Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.