Статьи

Тестирование интерфейса инвариантов

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

Мой проект

Из-за этого я собираюсь сделать краткое введение в то, что проект. Я называю это системой UDice. Это коллекция классов и интерфейсов для создания универсального ролика для игры в кости, который поддерживает любое и любое время механики штамповки и прокатки. Большая часть специфики проистекает из дизайна иерархии кристаллов, но некоторые из очень крутых функций потребуют специальных классов Roller .

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

Пример

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

Наш интерфейс верхнего уровня — Die (единственное число в кости; не убийственные предложения), который содержит несколько Face , точно так же, как в реальной жизни кубик имеет несколько лиц. Face есть несколько FaceValue которые не важны для вас; вам просто нужно знать, что они существуют — и вес, который регулирует частоту вращения Face . Вес служит двойной цели — изготовлению кубиков-мошенников, из-за которых определенные лица появляются чаще, чем они должны, и к тому, чтобы на матрице изображалось то же лицо несколько раз.

Я решил, что хочу, чтобы Die s всегда объединяли Face s в одно, когда есть дубликаты (при определении, являются ли Face s дубликатами друг друга, учитываются только FaceValue s), создавая новое Face с общим весом оригинальных двух. Но Die — это интерфейс, который не может это контролировать. Интерфейсы имеют методы по умолчанию для таких вещей, но они могут быть переопределены и не помогают в процессе создания объекта.

Абстрактные тесты

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
abstract public class AbstractDieTest
{
   /**
    * Creating a Die combines Faces with the same set of FaceValues into a
    * single Face with the same set of FaceValues and a combined weight of the
    * two original Faces
    */
   @Test
   public void creation1()
   {
      fail("test not implemented");
   }
     
   /**
    * Creating a Die does not combine Faces that have different sets of
    * FaceValues into a single Face
    */
   @Test
   public void creation2()
   {
      fail("test not implemented");
   }
}

Позвольте мне отметить несколько вещей:

  1. Класс абстрактный; Поскольку в тесте не указана конкретная реализация Die , и он предназначен для тестирования всех реализаций, его нельзя напрямую создать.
  2. Существует не только тест для проверки того, что он объединяет сходные лица, но также и то, что он не объединяет разнородные лица. Основная причина, по которой я разделил это на две части, заключалась в том, чтобы упростить отдельные тесты.
  3. Имена и комментарии, вероятно, отличаются от того, что вы видели раньше. Это мое личное предпочтение, о котором я говорил в старом посте . Он позволяет легко читать имена методов, позволяет полностью записывать действия теста с помощью комментариев, позволяющих использовать полную пунктуацию, и помогает легче находить тесты по их именам.

Малое производственное здание

Первый шаг в тесте — это этап сборки. Итак, нам нужно сделать несколько Die для тестирования. Как мы это делаем? Мы не знаем, как заставить конкретные реализации Die ; это всего лишь тест интерфейса. Решением является Фабрика, а именно Фабричный Метод. Итак, давайте сделаем это, мы будем?

1
abstract protected Die createTestDie(String name, Iterable faces);

Подробнее о вещах:

  1. Метод абстрактный. Я надеюсь, что это будет очевидно, но я чувствовал, что должен указать это на всякий случай. Тесты, которые наследуются от этого класса, будут теми, которые обеспечивают реализацию.
  2. Метод защищен. Нет никаких причин, чтобы какие-либо классы знали об этом методе, кроме подклассов, и они должны знать об этом только для того, чтобы создать его экземпляр для тестов этого класса.
  3. Я использую Iterable of Face s вместо какого-либо конкретного вида коллекции (или даже Collection ). Первая причина этого заключается в том, что единственное, для чего нам нужна «коллекция», — это итерация по ней, поскольку нам может потребоваться изменить коллекцию, комбинируя Face вместе. Вторая причина в том, что у меня есть свой собственный набор коллекций, которые не наследуются от Collection потому что Collection навязывает вам слишком много методов, и я хочу, чтобы мои коллекции были неизменяемыми, что означает, что все дополнительные методы будут не реализованы (что является раздражает).

Отсюда мы просто вызываем фабричный метод из наших тестов, чтобы построить то, что нам нужно, и пишем остальные наши тесты.

Документация

Последний шаг — документировать класс абстрактного теста и интерфейс, говоря, что когда кто-то пишет реализацию интерфейса с тестами, его тесты должны наследоваться от вашего класса абстрактного теста.

Outro

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