После прошлой статьи были пожелания увидеть применение unit-тестирования. Поэтому я решил написать небольшое приложение, используя тесты, и подробно его разобрать.
Скорее всего, у данной статьи появятся комментарии о BDD, что я не покрываю должный процент кода, либо еще какие-то замечания — я только за. Таким образом, помимо прочтения статьи люди получат пользу еще и от прочтения комментариев и, возможно, узнают больше о тестировании кода. Я не буду пытаться объяснить TDD — для этого есть специальная литература, а сам подход от смены платформы или языка сильно не поменяется.

Я решил отказаться от первоначальной идеи с «бизнес» приложением в пользу игрушки «крестики-нолики». Приложение будет состоять из нескольких этапов разработки.

Правила игры

2 игрока (игрок1 и игрок2) играют на поле 3х3. Ходы делаются поочередно. Начинает игрок1. Игрок1 ставит метку «крестик» на любую свободную клетку поля. Игрок2 ставит метку «нолик» на любую свободную клетку поля. Побеждает игрок, создавший своим ходом линию (вертикаль/горизонталь/диагональ) своей метки (крестик/нолик).

Выделение задач


  1. Отображение поля
  2. Конвертирование точки экрана в координаты поля
  3. Отображение меток игрока
  4. Хранение меток в виде данных
  5. Поиск последовательности меток


Прототип

Дает на ранних этапах понимание направления в разработке приложения.
Для написания этого этапа я не считаю необходимым выполнение пункта «Отображение поля».
Пункт «Отображение меток игрока» должен выполняться самым простым способом.
А пункт «Поиск последовательности меток» должен просто определить наличие последовательности необходимой длинны.

Перед продолжением я бы рекомендовал ознакомится с предыдущей статьей тем, кто не знает ничего о XCTest и OCMock. Так же за время написания статьи появилась еще статья по данной теме.
Исходники данного этапа находятся тут, а здесь я лишь разберу определенные моменты.

XOBaseTestCase
Данный класс является родительским для всех наших классов-тестов. Смысл данного класса — обеспечить информацию о падении внутри метода setUp. Ведь этот метод вызывается перед выполнением любого теста и падение здесь означает провал последующего теста. Однако сам метод не является тестом и падение в нем не запишет ошибку в таблицу тестов. Поэтому в данном классе имеется тест testSetUp. Данный тест выполнится успешно и будет вызван (в произвольном порядке) у каждого дочернего класса при успешном выполнении метода setUp. Провал этого теста сигнализирует о падении внутри метода setUp.

XOGridTest
Данный класс проверяет результат работы преобразования координат в координаты сетки.
Мы создаем экземпляр класса XOGrid и задаем ему некий фиксированный frame (0, 0, 30, 30) и задаем сетку 3x3.
Теперь мы хотим проверить правильность преобразования. Мы знаем, что точка (x=21, y=12) имеет координаты (y=1, x=2), поэтому при вызове метода отдаем заданную нами точку и проверяем результат с известным нам значением.
Если изменение работы класса XOGrid приведет к неверной работе преобразования, данный тест не будет пройден.

XOGridModelTest
В данном классе мы проверяем работу класса XOGridModel (который хранит метки в виде данных). А именно, мы проверяем состояние всего поля после создания экземпляра класса, проверяем работу получения и сохранения состояния клетки поля. Так же проверяем попытку получить значения не валидного участка поля.

XOBoardViewTest
Класс XOBoardView имеет зависимость от класса XOGrid. Происходит обращение к экземпляру класса XOGrid для получения конвертированных координат. Мы сами установили эти координаты и по вызову метода конвертирования возвращаем нужные нам координаты. Так же мы проверяем, действительно ли был вызван данный метод.

XOSequenceFinderTest
XOSequenceFinder самый необходимый класс для тестирования. Содержит больше всего логики. Плюс еще и имеет зависимость от класса XOGridModel. Содержит простые тесты, но большего кол-ва. Тесты на проверку по вертикали, горизонтали, диагоналям.
Реализация поиска на диагоналях в классе XOSequenceFinder имеет не самый понятный код, однако тесты проходит. Поэтому, в последующем можно заменить на более удачное решение и убедится, что все работает правильно.

Замечание
Класс XOViewController не имеет тестов. В текущий момент он не имеет публичных методов. Можно вынести onPanBoard: в заголовочный файл, но раскрывать это ради написания теста на мой взгляд в данный момент не имеет смысла.
Необходимость класса TestingXOAppDelegate описывается в конце статьи.


Данный прототип крайне простой, мы просто нажимаем на экран и 1/9 экрана выделяется прямоугольником черного либо серого цвета (в зависимости от игрока). Однако, этого достаточно для демонстрации работоспособности алгоритмов.

Изменение ТЗ


Как это часто бывает, после прототипа немного меняется ТЗ.
Я показал прототип моей девушке и на предложение придать приложению очертание получил следующее:
Пусть на фоне будет лист бумаги и сетка будет на ней нарисована под углом, а вместо прямоугольников будут привычные крестик и нолик. А когда собирается линия, то пусть она показывается.


Ну, хорошо, видимо как-то так должно быть
image

Отображение крестика, нолика, линии и поля связано с UI, но вот нахождение линии меток и наклон сетки — это уже вычисления.

Здесь имеется определенный нюанс в том, что на рисунке «тетрадка» не имеет наклона, однако сетка для игры имеет наклон. Крестики и нолики так же не наклонены, однако смещены в отношении сетки. Это вносит интерес в написание вычислений.

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



А теперь опять комментарии по определенным моментам. Их не так много.

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

разберем 1 тест подробно
/*
 *  before
 *      1 2 3
 *      4 5 6
 *      7 8 9
 *
 *  after 90 degree
 *      7 4 1
 *      8 5 2
 *      9 6 3
 */

- (void)testRotateOn90Degree {
    self.grid.degree = 90; //повернем до состояния 'after 90 degree' как указано в комментарии выше
    
    CGPoint point = CGPointMake(0.0f, 25.0f); //эта точка будет перевернута в точку (30.0f, 25.0f)
    XOGridPoint expected = XOGridPointMake(2, 2); //и соотвествовать данной координате

    XOGridPoint result = [self.grid convertPointToGridPoint:point]; //возьмем координату с поворотом на 90 градусов
    XCTAssertTrue(XOGridPointEqualToGridPoint(result, expected)); //совпала ли координата в наклонной сетке и ожидаемый результат
}

Почитать про реализацию вычислений можно здесь.

XOSequenceFinderTest
Класс SequenceFinder высчитывает наличие последовательности, либо координаты последовательности. Написать тесты очень просто для данного класса, а писать, имея наборы тестов, подобные вещи намного проще. Однако из-за расчетов 4х направлений тестов довольно много. Активно используются pragma mark.
Так же имеются генераторы данных. Не вызов setUp, а именно отдельные вызовы. Зачем? Я не хочу хранить столько полей, которые используются не настолько часто. Тесты, как и основной код, имеют свойство разрастаться, терять актуальность. Их так же, как и основной код, необходимо поддерживать.


Ссылки:
Разбор библиотеки для тестирования
Статья от Rambler
Прототип
Изменение ТЗ

В принципе, на этом все.
Напоминаю еще раз, что целью данной статьи являлась демонстрация применения unit-тестов. Приведены небольшие комментарии по использованию, выложены исходники 2х этапов разработки. Всем удачного котинга :-)

Комментарии (1)


  1. InstaRobot
    14.08.2015 12:58

    Ну чтож, что мне нравится в Хабре, что когда стоящее решение попадает в тренд, появляется много качественного материала! Спасибо за материал, однозначно в «Избранное». Если не трудно, пишите еще статьи, таким путем станет больше качественного кода у разработчиков.