Напишите модульные тесты и тесты пользовательского интерфейса для простого приложения-калькулятора… затем сделайте приложение

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

Машине потребовалось 15 минут, чтобы рассчитать 60-секундную траекторию снаряда, в то время как человеку потребовалось бы 40 часов, чтобы выполнить ту же задачу. Будучи ранним компьютером, ENIAC мог легко выйти из строя различными механическими способами, заставляя его выводить неверные данные.

В документальном фильме Совершенно секретные розы она объясняет процесс тестирования, который они использовали:

Рут [Тейтельбаум] и Марлин [Мельцер] должны были рассчитать траекторию точно так же, как это сделал ENIAC. Итак, у нас была тестовая программа… мы запускали тестовую программу, и, если она удалась, мы запускали реальную траекторию, а затем снова запускали тестовую траекторию… был между бы. Эта траектория, которую сделали Марлин и Рут, сделала нас полезными в глазах инженеров, потому что с помощью этой программы мы могли отладить ENIAC до вакуумной лампы!

Этот процесс должен быть знаком каждому, кто когда-либо писал модульный тест.

Если вы знаете, для чего предназначена функция, вы должны иметь возможность вводить данные с известным результатом и получать в конце результат, который вы ожидали.

Разработка через тестирование (TDD) доводит это до логического завершения: если вы еще не знаете, как тестировать свой код, вам даже не стоит его писать.

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

При написании тестов для этого простого приложения-калькулятора я забыл тот факт, что Double представлено в String с 6 завершающими нулями. Когда мои тесты провалились, мне понравилось, как это выглядело, поэтому я вернулся и изменил свои тесты, чтобы ожидать 6 знаков после запятой. Несмотря на то, что в TDD требуется множество таких незначительных настроек, большая часть структуры и логики тестов остается неизменной.

Гораздо проще модифицировать существующие тесты, чем писать тесты с нуля.

Если вы ждете, пока ваше приложение не будет готово, чтобы написать тесты, у вас может не быть времени или мотивации, чтобы написать их должным образом или подробно.

Давайте начнем

Я создал новый проект Xcode под названием TDDCalculator, но вы можете назвать свой проект как хотите.

Идентификатор организации также не имеет значения, так как мы не будем пытаться отправить это приложение в App Store.

Однако важно убедиться, что SwiftUI является выбранным интерфейсом, и установлен флажок «Включить тесты». Тесты можно добавить позже, но гораздо проще поставить галочку и иметь готовые примеры тестов Apple, ожидающие создания проекта. Проект должен поставляться с файлом с именем ContentView.swift по умолчанию, который содержит базовый пользовательский интерфейс «Hello world».

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

Написание UI-тестов

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

Если вы не знакомы с тестированием пользовательского интерфейса, это подход «черного ящика», когда тесты не имеют доступа к коду.

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

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

Он сможет выполнять операции сложения, вычитания, умножения и деления, как видно из enum в начале примера кода ниже. Каждый случай этого enum имеет необработанное значение, которое представляет оператор как String, и они описываются словами в основном потому, что операторы «+» и «-» не могут использоваться в идентификаторах в Swift.

У нас также есть функция, которая выполняет довольно простую задачу. Когда мы даем ему число, оно разбивает это число на отдельные цифры. Каждая цифра набирается вызовом app.buttons[String(digit)].tap(). Поскольку каждая цифра является лишь частью String, ее необходимо явно преобразовать обратно. Затем мы полагаемся на XCUIElementQuery, который будет искать в иерархии представлений кнопку с этой цифрой в качестве метки доступности. Любая кнопка с заголовком, а не пользовательская фигура или изображение, по умолчанию имеет этот заголовок в качестве метки доступности.

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

Ввод чисел — это только часть процесса расчета.

Нам нужна тестовая функция, которая знает, когда даны два числа и оператор, какие кнопки нажимать и какой результат мы можем ожидать. Operator enum уже было создано с необработанными значениями String, соответствующими кнопкам, поэтому мы можем передать одно из них в функцию и использовать это необработанное значение для поиска кнопки. Кнопка равенства не является одним из операторов, так как нам не нужна ситуация, в которой можно запросить результат «вычисления», например 2 = 2.

если вы не знакомы с термином операнд, это просто означает числа, которые мы используем в расчетах по обе стороны от оператора (плюс, минус, время или символ деления).

Наконец, switch используется для выполнения вычисления и возврата результата, который будет использоваться для сравнения с тем, что появляется на экране.

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

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

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

Если вы еще не видите здесь свои тесты, создайте проект для тестирования, нажав Cmd + Shift + U. Сборка с целью тестирования включает в себя создание полностью отдельных целей из вашего приложения, поэтому сборка проекта обычно не обновляет список тестов и не удаляет устаревшие сообщения об ошибках в вашем тестовом коде.

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

На ленте кода также есть кнопки в форме ромба, как видно на этом снимке экрана в строке 10.

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

Написание модульных тестов

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

Важно отметить, что мы импортируем TDDCalculator (или как там называется ваш проект) как @testable, так как без этого классы и функции будут недоступны.

Без него классы будут отображаться как internal, даже если они помечены как public.

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

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

Создайте файл с именем CalculatorViewModel.swift в главной папке вашего приложения, а не там, где находятся ваши тесты. Нам нужно снова создать перечисление Operator, так как основное целевое приложение не будет знать о том, что находится в целевом тесте пользовательского интерфейса.

Затем я добавил протокол Calculatable, который определяет, какими будут свойства и как будет выглядеть сигнатура функции.

Протокол также соответствует протоколу ObservableObject, что означает, что наш пользовательский интерфейс будет автоматически обновляться при изменении данных в CalculatorViewModel. Предоставленное мной CalculatorViewModel использует оформление @Published, чтобы показать, что мы хотим, чтобы SwiftUI обращал внимание на изменения в этих свойствах.

Предоставленная мной функция calculate(_,_,_) всегда будет возвращать -1, и для этого есть простая причина.

Я хотел сделать здесь минимум, чтобы я мог написать свои (неудачные) тесты, и это именно то, что я собираюсь сделать дальше.

Поскольку не нужно нажимать или смахивать, все, что нам нужно сделать, это сравнить результаты CalculatorViewModel.calculate(_,_,_) с результатами TDDCalculatorTests.calculate(_,_,_). Мы можем сделать это с помощью XCTAssertEqual, которое принимает два значения и не проходит тест, если они разные.

Если вы хотите запустить модульные тесты и увидеть, как они терпят неудачу, продолжайте. Ни один из ответов не равен -1, и мы написали CalculatorViewModel, чтобы вернуть это значение, несмотря ни на что. Когда вы будете готовы сделать версию функции calculate, которая действительно работает, проверьте ее ниже. Основное различие между этой и предыдущими итерациями функции заключается в том, что она устанавливает operatorType в ноль, operand1 в ответ (на случай, если вы хотите выполнить больше операций) и сбрасывает operand2 в ноль.

Ответ возвращается, чтобы можно было обновить пользовательский интерфейс.

"Какой пользовательский интерфейс?" спросите вы.

Мы собираемся его создать!

Создание пользовательского интерфейса

В каждом калькуляторе есть дисплей, на котором отображается последнее введенное число. Мы не собираемся предоставлять кнопку C или AC для очистки ввода, поэтому число просто останется на экране, пока мы не укажем операцию. Все, что делает этот вид, — отображает число, выровненное по правой стороне экрана, с контуром прямоугольника со скругленными углами. Используется шрифт Menlo, моноширинный шрифт, в котором все цифры отображаются одинаковой ширины.

Используя Color.primary, мы избегаем проблем с темным режимом, так как он будет отображаться черным в светлом режиме и белым в темном режиме.

Последний фрагмент кода довольно длинный, но он содержит в основном весь пользовательский интерфейс.

Начнем с CalculatorButtonStyle, который будет применяться к каждой кнопке в интерфейсе. По сути, это делает противоположное тому, что DisplayView делает с точки зрения Color, используя UIColor.systemBackground, чтобы дать нам белый цвет в светлом режиме и черный в темном режиме. Соотношение сторон кнопок установлено равным 1, чтобы принудительно отображать их в виде закругленных квадратных форм.

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

Адаптивный GridItem определяется, чтобы указать, какими должны быть наши столбцы.

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

При касании оператора дисплей очищается, поэтому можно ввести новый номер.

Но что происходит, когда отображается ответ?

Ввод нового числа должен очистить ответ, так как мы не хотим добавлять к нему новые цифры.

Я сделал это довольно грубо с булевым значением answerShown, которое устанавливается в false, как только набирается новое число.

Вот как это должно выглядеть:

Если вам нужно увидеть полный код, он доступен на GitHub.

Если в примерах кода есть ошибки или если у вас возникли проблемы с работой, сообщите мне об этом в ответе ниже.

Следующие шаги

Вы знаете, что обычно может делать калькулятор, и TDD на этом не останавливается. Если вы хотите добавить кнопку с десятичной точкой, кнопку C/AC или даже кнопки M+/M-/MRC, эта функция начинается с тестов.

Что вы ожидаете от пользовательского интерфейса, когда вы нажимаете эти кнопки?

Как ваши модульные тесты гарантируют, что код вычисляет или возвращает значения так, как вы ожидаете?

Независимо от того, рассчитываете ли вы траектории оболочки на ENIAC или создаете кроссплатформенное приложение Swift с помощью MacBook Pro M1 Max, тестирование вашего кода становится намного проще, если тесты уже существуют до того, как вы начнете программировать.