Тестирование — один из основных способов выявления проблем в коде для их быстрого устранения и снижения издержек. В большинстве случаев при организации контроля качества лучше задействовать сразу несколько вариантов автоматизированного тестирования — тестов одного вида для проверки целого приложения или его большого компонента недостаточно.
Меня зовут Евгений Плёнкин. Я iOS разработчик компании СберЗдоровье. В этом материале я расскажу, что такое автоматизированное тестирование, в чём его польза в iOS-разработке и не только, сколько и каких тестов должно быть, а также какой инструмент для автотестов в iOS есть.
Статья написана в рамках серии «Модульное тестирование в iOS: все, что надо знать».
Что такое автоматизированное тестирование
Автоматизированное тестирование – это процесс проверки программного обеспечения, при котором основные функции и шаги теста (от запуска до выдачи результатов) автоматически выполняют специализированные инструменты.
С помощью автоматизированного тестирования можно делегировать компьютеру часть трудоемких и рутинных задач. Кроме того, работа с автотестами несёт ряд выгод для всех участников разработки.
Для владельца продукта
Согласно исследованию Microsoft, внедрение практик автоматизированного тестирования позволяет на 20.9% сократить число ошибок, попадающих в релиз и выявляемых при эксплуатации готового продукта. По другим оценкам, постоянное проведение автотестов может сократить количество таких дефектов до 90%. При этом, по данным Systems Sciences Institute at IBM, стоимость устранения бага, обнаруженного после релиза программы, в 4-5 раз выше, чем при его устранении на стадии проектирования.
То есть, проведение автоматизированного тестирования помогает сократить расходы на поддержку, а также повысить лояльность пользователей, получающих более стабильное ПО.
Для тестировщика
Работа QA-специалиста ориентирована на выявление всех дефектов — от простых и очевидных до сложных и нетипичных. Но, если простых ошибок много, сложные и объёмные баги могут остаться без внимания — на них банально может не хватить времени тестировщика.
Автоматизация тестирования помогает переложить поиск простых и очевидных ошибок на специальные инструменты, а QA-специалистам дать время на исследовательское тестирование и проверку сложных сценариев.
Это подтверждает и статистика из исследования Microsoft — в проектах, в которых используют автоматизированное тестирование, как простых, так и сложных ошибок становится меньше. В свою очередь это делает работу QA интересней.
Для разработчика
Преимуществ для разработчика сразу несколько.
Понимание требований при разработке. Написание тестов помогает глубже разобраться в работе с кодом.
Стабильность при рефакторинге. Тесты гарантируют, что при рефакторинге ничего не сломается и не надо будет тратить много времени на проверку корректности кода.
Самодокументирование. Тесты помогают разобраться в назначении модулей и классов.
Больше интересных задач. Меньше багов — больше времени на бизнес-задачи.
Согласно данным опроса, тесты помогают создавать более качественный код и быстрее находить источник дефекта.
Подробнее о выгодах проведения тестов можете прочитать в нашем предыдущем материале.
Виды тестов
Одно из недавних исследований показало, что сейчас почти 2/3 iOS-разработчиков пишут модульные тесты. Ещё 5 лет назад таких было всего 3%. Это свидетельствует о том, что индустрия быстро меняется и знание тестов стало обязательным навыком даже для специалистов уровня Junior.
По степени изолированности функциональности выделяют 4 вида тестирования: модульное, интеграционное (два подтипа: компонентное и системное), приёмочное и UI-тестирование.
Модульное (оно же Unit) — тестирование отдельного класса или метода. Используют, чтобы найти участок кода, вызывающий проблему и устранить её.
Компонентное интеграционное тестирование — проверяет связи между компонентами.
Системное интеграционное тестирование (E2E) — проверяет связи между под-системами / системами. Его не всегда можно автоматизировать, так как часто интеграция происходит с внешним сервисом, к которому нет доступа.
Приёмочное — также может быть автоматизировано лишь частично. Отличается от системного интеграционного своей целью: проводится, чтобы проверить, удовлетворяет ли система приёмочным критериям и вынесения решения заказчиком или другим уполномоченным лицом, принимается приложение или нет.
UI-тестирование — проверка интерфейса (pixel perfect) на соответствие конкретного экрана заданному шаблону.
Сценарии, которые не получается автоматизировать, относятся к ручному тестированию.
Разработчики чаще всего пишут модульные и интеграционные тесты. Остальное или остается на ручной проверке QA-специалистов, или не проверяется вовсе.
Сколько и каких тестов должно быть в проекте
При покрытии приложения тестами важно правильно подобрать их количество и соотношение. Один из основных подходов определения соотношения — пирамида автоматизации тестирования, описанная Майком Коном.
По правилам пирамиды Кона, соотношение тест-кейсов должно быть распределено следующим образом:
модульные — 80%;
интеграционные — 10%;
приёмочные — 5%;
интерфейсные — 5%.
Ручное тестирование рассматривают отдельно — зачастую эти задачи выполняют пользователи на предварительных и бета-версиях продукта.
Но на практике идеальная пирамида встречается редко. Зачастую это нечто среднее:
рожок — мало юнит-тестов и очень много ручного тестирования;
рюмка — много юнит-тестов и ручного тестирования без низкоуровневой автоматизации.
Рожок и рюмка — не лучшие модели распределения тестов в iOS. Их желательно приводить к виду пирамиды. Основная причина — соотношение времени и расходов. Так, на модульные тесты надо в сотни раз меньше времени и ресурсов, чем на любые верхнеуровневые. С повышением уровня тестирования увеличивается охват системы, но выявлять простые ошибки на сложных тестах нерационально. Поэтому модульных тестов должно быть много, а приемочных и ручных — минимум.
Инструмент тестирования
Основной инструмент тестирования для iOS-разработчиков — фреймворк XCTest. Это библиотека Apple, которая построена на принципах xUnit архитектуры модульных тестов и входит в стандартный SDK. XCTest — наследник фреймворка SenTestingKit, который Apple забрала к себе в 2005-м году и постепенно развивала. Публично доступен с Xcode 5.
Архитектура инструмента включает:
test runner — модуль, который запускает тест и отображает прогресс его выполнения;
test case — тестовые сценарии, которые являются базой для тестов;
test fixture — конфигурации тестирования, то есть набор заданных условий или состояний объектов, которые нужны для запуска тестов;
test suite — наборы тестов с одинаковой конфигурацией;
asserts — сущности, которые отвечают за реализацию утверждений.
Комбинация описанных выше элементов, входящих в архитектуру, формирует структуру прогона.
Самый важный шаг приведённой выше схемы — выполнение теста (performTest). В нем:
формируется проверяемый объект — system-under-test (SUT), — который и подвергается тестированию;
выдвигаются утверждения (asserts), сравнивающие ожидаемое и фактическое поведения SUT.
Алгоритм работы инструмента следующий:
При запуске таргета с модульными тестами код компилируется и запускается раннером.
-
Test runner находит все классы-наследники XCTestCase, создает их экземпляры, в каждом из которых есть методы performTest. Согласно архитектуре xUnit, эти методы должны соответствовать сигнатуре:
func test<Name>() -> Void
Название начинается со слова test, список входных параметров пуст, выходное значение — Void.
Далее идёт последовательность запусков конкретных проверок из экземпляров XCTestCase: метод setUp, метод test, метод tearDown, повторно setUp, следующий test и так далее.
В рамках прогона test-runner помечает результаты из каждого XCTestCase и фиксирует их в консоли или в xcresult — файле специального формата с информацией о тестах и логах, скриншотами, замерами покрытия таргетов и так далее.
Assert’ы — ядро для организации проверок поведения системы (SUT) – имеют следующий вид:
public func XCTAssertTrue(
_ expression: @autoclosure() throws -> Bool,
_ message: @autoclosure() -> String = "",
file: StaticString = #filePath,
line: UInt = #line
)
public func XCTAssertEqual<T>(
_ expression1: @autoclosure() throws -> T,
_ expression2: @autoclosure() throws -> T,
accuracy: T,
_ message: @autoclosure() -> String = "",
file: StaticString = #filePath,
line: UInt = #line
) where T : FloatingPoint
Название каждого assert’a начинается с букв XCT, далее идёт обозначение, что это именно assert, а после — описание его работы. Если это XCTAssertTrue, то он ожидает, что поступившее на вход выражение будет правдиво, и, если оно таким не будет, тест будет провален. Если использовать XCTAssertEqual, то для успешного прохождения теста входящие выражения должны быть равны. Рассмотрим на примерах.
Пример 1.func test_twoNumbersEqualWithAccuracy() { // 1
XCTAssertEqual(0.01, 0.02, accuracy: 0.01) // успех // 2
}
1 — задаём название теста, начинающееся со слова test. Дальше идёт краткое описание, что он делает. В данном случае происходит сравнение двух чисел с точностью.
2 — описываем assert, куда передаются два числа и указываем точность. В результате сравнения этих чисел, мы на выходе получаем пройденный тест — указанная точность говорит о том, что эти два числа одинаковые.
Пример 2.func test_twoNumbersEqualWithAccuracy() {
XCTAssertEqual(0.01, 0.02, accuracy: 0.001) // провал
}
Тут такой же тест, только изменилась точность. Как итог — тест провален, поскольку с такой точностью эти числа уже не равны.
Структура тестовой проверки
Назначение любого метода в коде должно быть понятно из его названия. Так и с конкретными тестовыми проверками — первое слово test, а далее идет описание того, что именно метод тестирует. В название могут быть добавлены особые условия, если они есть, а также описание ожидаемого результата.
Тестовый метод (конкретная тестовая проверка), как правило, имеет трёхактную структуру, которая может быть реализована с помощью разных подходов: Given – When – Then (GWT) или Arrange-Act-Assert (ААА). Я предпочитаю ААА.
В Arrange секции инициализируют объекты и устанавливают значения в данные, которые будут переданы в тестируемый метод.
В Act секции выполняется тестируемый метод или группа тестируемых методов у SUT с подготовленными параметрами.
В Assert секции проверяется, что тестируемый метод ведёт себя так, как ожидается: выдвигаются предположения, истинность которых будет установлена при прогоне.
Пример:
func test_whenWrongPhone_thenIsNotValid() { // 1
// arrange
let wrongPhone = "7 111 111 11 11" // 2
// act
let result = sut.isValid(wrongPhone) // 3
// assert
XCTAssertFalse(result) // 4
}
Здесь:
1 — название теста начинается со слова test, затем оговариваются условия: когда телефон не верен, он не должен пройти проверку.
2 — в разделе arrange мы подготавливаем данные, в данном случае это заранее неправильный номер телефона (не вдаваясь сейчас в подробности, что именно с ним не так).
3 — в разделе act мы вызываем у тестируемого объекта (SUT) тестируемый метод.
4 — тестируемый метод должен возвращать значение провала, подтверждая тем самым теорию, что проверка на корректность работает.
Немного автоматизации
Структура теста всегда похожа: схожие импорты, схожие названия, одно наследование, обязательные методы и… Неработающая нотация ААА из коробки!
Для этого рекомендую создать шаблон xctemplate. И тогда любой, вновь создаваемый файл тестового сценария может выглядеть как-то так:@testable import SberHealth
import XCTest
final class SUTTests: XCTestCase {
// MARK: - Properties
private var sut: SUT!
// MARK: - Setting up the environment
override func setUp() {
super.setUp()
}
override func tearDown() {
sut = nil
super.tearDown()
}
// MARK: - Tests
func test_when_then() throws {
// arrange
// act
// assert
}
}
Когда и для чего писать тесты
Есть два принципа — test first и test last. Они определяют, когда писать тест — до кода или после. На практике порядок написания редко имеет значение — намного важнее, чтобы тест был написан в рамках одного коммита с production-кодом. Это удобно и гарантирует, что код или функция будут проверены.
Тесты надо писать для каждой нетривиальной функции или метода. Это важно для быстрой проверки, не привело ли очередное изменение кода к регрессии, то есть к появлению ошибок в уже протестированных местах программы.
Есть всего несколько причин, из-за которых можно не писать тесты: сжатые сроки, ограниченный бюджет, простые требования к коду, отсутствие сложной логики. В двух первых случаях (сжатые сроки и ограниченный бюджет) отсутствие тестов обусловлено бизнесовыми издержками, в случае простых требований к коду и простой логики — технической нецелесообразностью тратить время на тесты. Во всех остальных случаях долгосрочные проекты должны обязательно покрываться тестами. При этом, тесты должны быть:
достоверными;
атомарными: один тест проверяет только одну вещь;
независимыми от окружения, на котором выполняются;
простыми в поддержке;
лёгкими для чтения и понимания (даже новый разработчик должен понять, что именно тестируется).
Отдельно стоит учитывать требования к покрытию тестами. В общем случае есть четыре вида кода:
простой код без зависимостей;
сложный код с большим количеством зависимостей;
сложный код без зависимостей;
не очень сложный код с зависимостями.
В каждом из случаев будет отличаться сложность и количество тестов, но здесь важнее степень покрытия кода тестами. В СберЗдоровье мы предполагаем следующие рекомендации:
60% — приемлемо;
75% — похвально;
90% — образцово.
Саммари
Автоматизированные тесты в iOS — не привилегия, а необходимость. Их внедрение одновременно упрощает работу технических специалистов и помогает получать реальные бизнес-выгоды.
Есть много видов тестов. Основа любого тестирования — модульные, которые требуют меньше времени и ресурсов, а также могут быть проведены с помощью понятного стека без привлечения QA-специалистов.
В следующих статьях я расскажу о чистых тестах, частых ошибках и возможностях нативных iOS-инструментов тестирования.