Туториал по Unit и UI-тестированию в iOS

29 апреля 2021

В этом туториале вы узнаете, как добавить unit тесты и UI тесты в свои приложения для iOS, и как вы можете самостоятельно проверить покрытие кода.

Заметка

Информация по обновлению: Дэвид Пайпер обновил этот туториал для Xcode 12.4, Swift 5.3 и iOS 14. Одри Тэм является автором оригинала.

Unit тестирование в iOS не очень гламурная вещь, но, поскольку тесты не позволяют вашему блестящему приложению превратиться в мусор из-за наличия ошибок, являются необходимыми. То, что вы читаете этот туториал говорит о том, что вы знаете, что писать тесты для своего кода и UI нужно, но, возможно, не знаете, как именно это сделать.

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

Из этого туториала вы узнаете как:

  • Использовать Test navigator в Xcode для тестирования модели приложения и асинхронных методов.
  • Имитировать взаимодействие с библиотекой или системными объектами с помощью заглушек и моков.
  • Тестировать UI и производительность.
  • Использовать инструмент покрытия кода.

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

Приступим

Начните с загрузки материалов проекта, используя кнопку Download Materials. Он включает проект BullsEye, основанный на примере приложения в UIKit Apprentice. Это простая игра на везение и удачу. Логика игры находится в классе BullsEyeGame, который вы протестируете в этом туториале.

Выясняем, что именно будем тестировать

Прежде чем писать тесты, нужно знать что тестировать?

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

Как правило, тесты должны охватывать:

  • Основные функции: классы и методы модели и их взаимодействие с контроллером.
  • Наиболее распространенные рабочие процессы UI
  • Пограничные условия
  • Исправление ошибок

Какими должны быть тесты

Аббревиатура FIRST описывает краткий набор критериев для эффективных unit тестов. Этими критериями являются:

  • Fast (Быстрота): тесты должны выполняться быстро.
  • Independent/Isolated (Независимость/изолированность): тесты не должны зависеть друг от друга.
  • Repeatable (Повторяемость): вы должны получать одни и те же результаты при каждом запуске теста. Внешние источники данных или проблемы с параллелизмом могут вызывать периодические сбои.
  • Self-validating (Самостоятельная проверка): тесты должны быть полностью автоматизированы. Результат должен быть либо «прошел», либо «не прошел», без необходимости полагаться на интерпретацию разработчиком лог-файла.
  • Timely (Своевременность): в идеале вы должны писать тесты до написания продакшн кода, который они тестируют. Это называется разработкой через тестирование.

Следование принципам FIRST сделает ваши тесты понятными и полезными, а не превратит их в препятствие для вашего приложения.

Unit тестирование в Xcode

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

Создаем Unit Test Target

Откройте проект BullsEye и нажмите Command-6, чтобы открыть Test navigator.
Щелкните на + в нижнем левом углу, затем выберите New Unit Test Target… в меню:

 

Примите дефолтное имя, BullsEyeTests, и введите com.raywenderlich в качестве Organization Identifier (идентификатора организации). Когда test bundle появится в Test navigator, разверните его, щелкнув треугольник раскрытия и нажав BullsEyeTests, чтобы открыть его в редакторе.

 

 

Дефолтный шаблон импортирует фреймворк тестирования XCTest и определяет подкласс BullsEyeTests класса XCTestCase с setUpWithError(), tearDownWithError() и примерами методов тестирования.

Вы можете запустить тесты тремя способами:

  1. Product ▸ Test и Command-U. И то и то запускает все тестовые классы.
  2. Щелкните на кнопку со стрелкой в Test navigator.
  3. Щелкните на кнопку ромбик как на рисунке.

 


Вы также можете запустить отдельный метод тестирования, щелкнув на его кнопку-ромбик в Test navigator.
Попробуйте разные способы запуска тестов, чтобы понять, сколько времени это занимает и как это выглядит. Образцы тестов пока ничего не делают, поэтому они выполняются очень быстро!
Когда все тесты пройдут успешно, ромбы станут зелеными и появятся галочки. Щелкните на серый ромбик в конце testPerformanceExample(), чтобы открыть Performance Result (итоговая производительность).

В этом туториале вам не нужны testPerformanceExample() или testExample(), поэтому удалите их.

Использование XCTAssert для тестирования моделей

Во-первых, вы воспользуетесь функциями XCTAssert, чтобы проверить основную функцию модели BullsEye: правильно ли BullsEyeGame вычисляет счет за раунд?
В BullsEyeTests.swift добавьте эту строку под import XCTest:

@testable import BullsEye

Это дает unit тестам доступ к внутренним типам и функциям в BullsEye.
Вверху BullsEyeTests добавьте это свойство:


var sut: BullsEyeGame!

Это создаст плейсхолдер для BullsEyeGame, который является System Under Test (SUT), или объектом, с которым этот тестовый кейс класса связан с тестированием.

Затем замените содержимое setUpWithError() следующим:


try super.setUpWithError()
sut = BullsEyeGame()

Это создает BullsEyeGame на уровне класса, поэтому все тесты в этом тестовом классе могут получить доступ к свойствам и методам объекта SUT.
Чтобы не забыть, сразу освободите объект SUT в tearDownWithError(). Замените его содержимое на:


sut = nil
try super.tearDownWithError()

Заметка

Рекомендуется создавать SUT в setUpWithError() и освобождать его в tearDownWithError(), чтобы быть уверенными, что каждый тест начинается с чистого листа. Дополнительную информацию можно найти у Джона Рида.

Написание первого теста
Вы готовы написать свой первый тест!
Добавьте следующий код в конец BullsEyeTest:


func testScoreIsComputedWhenGuessIsHigherThanTarget() {
// given (дано)
let guess = sut.targetValue + 5

// when (когда)
sut.check(guess: guess)

// then (тогда)
XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

Название метода тестирования всегда начинается с test, за которым следует описание того, что он тестирует.
Хорошо разделять тест на given, when и then разделы:

  1. given: Здесь вы устанавливаете любые необходимые значения. В этом примере вы создаете значение guess, чтобы указать, насколько оно отличается от targetValue.
  2. when: в этом разделе вы выполните тестируемый код: вызовите check(guess:).
  3. then: это раздел, в котором вы подтвердите ожидаемый результат с помощью сообщения, которое напечатается, если тест не пройден. В этом случае sut.scoreRound должен быть равен 95, так как это 100–5.

Запустите тест, щелкнув на ромбик в панели или в Test navigator. Это соберет и запустит приложение, а значок ромбика изменится на зеленую галочку! Вы также увидите, как на мгновение появляется всплывающее окно над Xcode, которое также указывает успешное завершение и выглядит следующим образом:

Заметка

Чтобы увидеть полный список XCTestAssertions, пройдите по ссылке: Apple’s Assertions Listed by Category.

Отладка теста

В BullsEyeGame специально встроена ошибка, и теперь вы попрактикуетесь в ее поиске. Чтобы найти ошибку, вы создадите тест, который вычитает 5 из targetValueв разделе given и оставляет остальное без изменений. Добавьте следующий тест:

func testScoreIsComputedWhenGuessIsLowerThanTarget() {
  // given
  let guess = sut.targetValue - 5

  // when
  sut.check(guess: guess)

  // then
  XCTAssertEqual(sut.scoreRound, 95, "Score computed from guess is wrong")
}

Разница между guess и targetValue по-прежнему равна 5, поэтому результат должен быть 95.
В Breakpoint navigator добавьте Test Failure Breakpoint (точку остановки при ошибке теста). Это остановит работу теста, когда метод теста отправит утверждение об ошибке.

 

Запустите тест, он должен остановиться на строке XCTAssertEqual ошибкой теста.
Обратимся к sut и guess в консоли отладки:

 

 

guess равен targetValue − 5, но scoreRound равен 105, а не 95!

Дальше используйте обычный процесс отладки: установите брейкпоинт в операторе when, а также в BullsEyeGame.swift, внутри check(guess:), где создается difference. Затем снова запустите тест и перешагните через оператор let difference, чтобы проверить значение difference в приложении:

 

 

Проблема в том, что difference отрицательная, поэтому результат 100 - (−5). Чтобы исправить это, вы должны использовать абсолютное значение difference. В check(guess:) раскомментируйте правильную строку и удалите неправильную.

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

Использование XCTestExpectation для тестирования асинхронных операций

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

BullsEyeGame использует URLSession для получения случайного числа в качестве таргета для следующей игры. Методы URLSession асинхронны: они возвращаются сразу, но не завершают свою работу позже. Чтобы протестировать асинхронные методы, используйте XCTestExpectation, чтобы ваш тест ждал завершения асинхронной операции.

Асинхронные тесты обычно проходят медленно, поэтому вы должны держать их отдельно от более быстрых модульных тестов.

Создайте новый unit test target с именем BullsEyeSlowTests. Откройте новый тестовый класс BullsEyeSlowTests и импортируйте модуль приложения BullsEye чуть ниже существующего оператора import:


@testable import BullsEye

Все тесты в этом классе используют URLSession по умолчанию для отправки запросов, поэтому объявите sut, создайте его в setUpWithError() и освободите в tearDownWithError(). Для этого замените содержимое BullsEyeSlowTests на:


var sut: URLSession!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = URLSession(configuration: .default)
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

Затем добавьте этот асинхронный тест:


// Asynchronous test: success fast, failure slow
func testValidApiCallGetsHTTPStatusCode200() throws {
  // given
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  // 1
  let promise = expectation(description: "Status code: 200")

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    // then
    if let error = error {
      XCTFail("Error: \(error.localizedDescription)")
      return
    } else if let statusCode = (response as? HTTPURLResponse)?.statusCode {
      if statusCode == 200 {
        // 2
        promise.fulfill()
      } else {
        XCTFail("Status code: \(statusCode)")
      }
    }
  }
  dataTask.resume()
  // 3
  wait(for: [promise], timeout: 5)
}

Этот тест проверяет, что отправка валидного запроса возвращает код состояния 200. Большая часть кода совпадает с тем, что вы пишете в приложении + эти дополнительные строки:

  1. expectation(description:): возвращает XCTestExpectation, хранящееся в promise. description описывает то, что вы ожидаете.
  2. promise.fulfill(): вызовите этот код в клоужере, отвечающем за успех завершающего обработчика асинхронного метода, чтобы показать, что наши ожидания были оправданы.
  3. wait(for:timeout:): тест продолжается до тех пор, пока не будут выполнены все ожидания или пока не закончится интервал timeout, в зависимости от того, что произойдет раньше.

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

Быстрый фейл

Фейл - это больно, но он не должен длиться вечно.
Чтобы произошел сбой, просто измените URL-адрес в testValidApiCallGetsHTTPStatusCode200() на невалидный:


let url = URL(string: "http://www.randomnumberapi.com/test")!

Запустите тест. Он фейлится, но требует полный интервал timeout! Это произошло потому, что вы предполагали, что запрос всегда будет проходить, и именно здесь вы вызвали promise.fulfill(). Поскольку запрос не удался, он завершился только по истечении тайм-аута.

Можно ускорить выполнение теста, изменив предположение. Вместо того, чтобы ждать успешного выполнения запроса, дождитесь только завершения обработчика асинхронного метода. Это происходит, как только приложение получает ответ (ОК или ошибка) от сервера. Затем ваш тест может проверить, был ли запрос успешным.

Чтобы увидеть, как это работает, создайте новый тест.

Но сначала исправьте предыдущий тест, отменив изменение, которое вы внесли в url.
Затем добавьте в свой класс следующий тест:


func testApiCallCompletes() throws {
  // given
  let urlString = "http://www.randomnumberapi.com/test"
  let url = URL(string: urlString)!
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?

  // when
  let dataTask = sut.dataTask(with: url) { _, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    promise.fulfill()
  }
  dataTask.resume()
  wait(for: [promise], timeout: 5)

  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

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

Запустите тест. Теперь это должно занять около секунды. Тест фейлится из-за фейла запроса, а не из-за превышения timeout во время выполнения теста.

Исправьте URL, а затем снова запустите тест, чтобы убедиться, что он успешно завершен.

Условный фейл

В некоторых ситуациях выполнение теста не имеет особого смысла. Например, что должно произойти, если testValidApiCallGetsHTTPStatusCode200() работает без сетевого подключения? Конечно, он не должен пройти, потому что он не получит код статуса 200. Но и он не должен провалиться, потому что ничего не проверял.
К счастью, Apple представила XCTSkip, чтобы можно было пропустить тест, если предварительные условия не выполняются. Добавьте следующую строку под объявлением sut:


let networkMonitor = NetworkMonitor.shared

NetworkMonitor обертывает NWPathMonitor, обеспечивая удобный способ проверки сетевого подключения.
В testValidApiCallGetsHTTPStatusCode200() добавьте XCTSkipUnless в начале теста:


try XCTSkipUnless(
  networkMonitor.isReachable, 
  "Network connectivity needed for this test.")

XCTSkipUnless(_: _ :) пропускает тест, если сеть недоступна. Проверьте это, отключив сетевое соединение и запустив тест. В поле рядом с тестом вы увидите новый значок, указывающий на то, что тест не прошел и не провалился.

 

 

Снова включите сетевое соединение и повторно запустите тест, чтобы убедиться, что он по-прежнему работает успешно в нормальных условиях. Добавьте тот же код в начало testApiCallCompletes().

Имитация объектов и взаимодействий

Асинхронные тесты дают вам уверенность в том, что ваш код генерирует правильный ввод для асинхронного API. Вы также можете проверить, правильно ли работает ваш код, когда он получает ввод от URLSession, или что он правильно обновляет базу данных UserDefaults или контейнер iCloud.
Большинство приложений взаимодействуют с объектами системы или библиотеки - объектами, которые вы не контролируете. Тесты, которые взаимодействуют с этими объектами, могут быть медленными и не повторяться, нарушая два принципа FIRST. Вместо этого вы можете имитировать взаимодействия, получая данные из stubs или обновляя mock-объекты.
Используйте имитацию, когда ваш код зависит от системного или библиотечного объекта. Сделайте это, создав фейковый объект, который играет эту роль, и вставьте этот объект в свой код. В Dependency Injection Джона Рида описывается несколько способов сделать это.

Имитация ввода из Stub

Теперь убедитесь, что getRandomNumber(completion:) правильно анализирует загруженные данные. Вы имитируете сеанс BullsEyeGame с помощью стаббов.

Перейдите в Test navigator, щелкните + и выберите New Unit Test Class…. Дайте ему имя BullsEyeFakeTests, сохраните его в каталоге BullsEyeTests и установите таргет на BullsEyeTests.

 

 

Импортируйте BullsEye модуль перед import:


@testable import BullsEye

Замените содержимое BullsEyeFakeTests этим:


var sut: BullsEyeGame!

override func setUpWithError() throws {
  try super.setUpWithError()
  sut = BullsEyeGame()
}

override func tearDownWithError() throws {
  sut = nil
  try super.tearDownWithError()
}

Это объявляет SUT, которым является BullsEyeGame, создает его в setUpWithError() и освобождает в tearDownWithError().
Проект BullsEye содержит вспомогательный файл URLSessionStub.swift. Он определяет простой протокол с именем URLSessionProtocol с методом создания data task с URL. Он также определяет URLSessionStub, который соответствует этому протоколу. Его инициализатор позволяет вам определять данные, ответ и ошибку, которые должна возвращать data task.
Для настройки этой имитации зайдите в BullsEyeFakeTests.swift и добавьте новый тест:


func testStartNewRoundUsesRandomValueFromApiRequest() {
  // given
  // 1
  let stubbedData = "[1]".data(using: .utf8)
  let urlString = 
    "http://www.randomnumberapi.com/api/v1.0/random?min=0&max=100&count=1"
  let url = URL(string: urlString)!
  let stubbedResponse = HTTPURLResponse(
    url: url, 
    statusCode: 200, 
    httpVersion: nil, 
    headerFields: nil)
  let urlSessionStub = URLSessionStub(
    data: stubbedData,
    response: stubbedResponse, 
    error: nil)
  sut.urlSession = urlSessionStub
  let promise = expectation(description: "Value Received")

  // when
  sut.startNewRound {
    // then
    // 2
    XCTAssertEqual(self.sut.targetValue, 1)
    promise.fulfill()
  }
  wait(for: [promise], timeout: 5)
}

В этом тесте:

  1. Вы настраиваете фейковые данные и ответ, а также создаете фейковый объект сеанса. Наконец, добавляете фейковую сессию в приложение как свойство sut.
  2. Вам все равно придется писать все это как асинхронный тест, потому что стаб претендует на роль асинхронного метода. Проверьте, парсит ли вызов startNewRound(completion:) фейковые данные, сравнивая targetValue с стаббированным фейковый номером. Запустите тест. Это должно произойти довольно быстро, потому что нет никакого реального сетевого подключения!

Имитация обновления для Мокированного объекта

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

  1. Переместить ползунок, чтобы угадать значение таргета.
  2. Угадать значение по положению ползунка.

Сегментед контрол правом нижнем углу переключает стиль игры и сохраняет его в UserDefaults.
Следующий тест проверяет, правильно ли приложение сохраняет свойство gameStyle.
Добавьте новый тестовый класс в таргет BullsEyeTests и назовите его BullsEyeMockTests. Добавьте следующее под оператором import:


@testable import BullsEye

class MockUserDefaults: UserDefaults {
  var gameStyleChanged = 0
  override func set(_ value: Int, forKey defaultName: String) {
    if defaultName == "gameStyle" {
      gameStyleChanged += 1
    }
  }
}

MockUserDefaults переопределяет set(_:forKey:) для инкрементации gameStyleChanged. Подобные тесты часто устанавливают переменную Bool, но инкрементация Int дает вам большую гибкость. Например, ваш тест может проверить, что приложение вызывает метод только один раз.

Затем в BullsEyeMockTests объявите SUT и мокированный объект:


var sut: ViewController!
var mockUserDefaults: MockUserDefaults!

Замените setUpWithError() и tearDownWithError() на:


override func setUpWithError() throws {
  try super.setUpWithError()
  sut = UIStoryboard(name: "Main", bundle: nil)
    .instantiateInitialViewController() as? ViewController
  mockUserDefaults = MockUserDefaults(suiteName: "testing")
  sut.defaults = mockUserDefaults
}

override func tearDownWithError() throws {
  sut = nil
  mockUserDefaults = nil
  try super.tearDownWithError()
}

Это создает SUT и фейковый объект и внедряет мокированный объект как свойство SUT.
Теперь замените два метода тестирования по умолчанию в шаблоне следующим образом:


func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()

  // when
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    0, 
    "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(
    sut,
    action: #selector(ViewController.chooseGameStyle(_:)),
    for: .valueChanged)
  segmentedControl.sendActions(for: .valueChanged)

  // then
  XCTAssertEqual(
    mockUserDefaults.gameStyleChanged, 
    1, 
    "gameStyle user default wasn't changed")
}

Утверждение when состоит в том, что флаг gameStyleChanged равен 0 до того, как метод тестирования изменит сегментед контрол. Итак, если утверждение then также верно, это означает, что set (_: forKey :) был вызван ровно один раз.
Запустите тест. Должно сработать.

UI тестирование в Xcode

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

В Test navigator добавьте UI Test Target. Убедитесь, что Target to be Tested это BullsEye, а затем примите имя по умолчанию BullsEyeUITests.

 

 

Откройте BullsEyeUITests.swift и добавьте свойство сверху класса BullsEyeUITests:


var app: XCUIApplication!

Удалите tearDownWithError() и замените содержимое setUpWithError() следующим:


try super.setUpWithError()
continueAfterFailure = false
app = XCUIApplication()
app.launch()

Удалите два существующих теста и добавьте новый под названием testGameStyleSwitch().


func testGameStyleSwitch() {    
}

Откройте новую строку в testGameStyleSwitch()и нажмите красную кнопку Record в нижней части окна редактора:

 

Это откроет приложение в симуляторе в режиме, который записывает ваши взаимодействия как тестовые команды. После загрузки приложения коснитесь в виде слайдера переключателя стиля игры и top label. Нажмите кнопку Xcode Record еще раз, чтобы остановить запись.

Теперь у вас три строчки в testGameStyleSwitch():


let app = XCUIApplication()
app.buttons["Slide"].tap()
app.staticTexts["Get as close as you can to: "].tap()

Рекордер создал код для тестирования тех же действий, которые вы тестировали в приложении. Тапните на стиль игры в сегментед контроле в top label. Вы будете использовать их как основу для создания собственного UI теста. Если вы видите какие-либо другие выражения, просто удалите их.

Первая строка дублирует свойство, которое вы создали в setUpWithError(), поэтому удалите эту строку. Пока вам не нужно ничего тапать, поэтому также удалите .tap() в конце строк 2 и 3. Теперь откройте небольшое меню рядом с ["Slide"] и выберите segmentedControls.buttons ["Slide"].

 

У вас должно остаться:


app.segmentedControls.buttons["Slide"]
app.staticTexts["Get as close as you can to: "]

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


// given
let slideButton = app.segmentedControls.buttons["Slide"]
let typeButton = app.segmentedControls.buttons["Type"]
let slideLabel = app.staticTexts["Get as close as you can to: "]
let typeLabel = app.staticTexts["Guess where the slider is: "]

Теперь, когда у вас есть имена двух кнопок в сегментед контроле и двух возможных верхних ярлыков, добавьте следующий код ниже:


// then
if slideButton.isSelected {
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)

  typeButton.tap()
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)
} else if typeButton.isSelected {
  XCTAssertTrue(typeLabel.exists)
  XCTAssertFalse(slideLabel.exists)

  slideButton.tap()
  XCTAssertTrue(slideLabel.exists)
  XCTAssertFalse(typeLabel.exists)
}

Этот код проверяет, существует ли правильный ярлык, когда вы tap() на каждой кнопке в сегментед контроле. Запустите тест - все выражения должны быть успешными.

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

Из документации Apple:
Тест производительности берет блок кода, который вы хотите оценить, и запускает его десять раз, собирая среднее время выполнения и стандартное отклонение для запусков. Усреднение этих отдельных измерений формирует значение для тестового прогона, которое затем можно сравнить с базовым уровнем для оценки успеха или неудачи.

Написать тест производительности просто: просто поместите код, который вы хотите измерить, в клоужер measure(). Кроме того, вы можете указать несколько показателей для измерения.

Добавьте в BullsEyeTests следующий тест:


func testScoreIsComputedPerformance() {
  measure(
    metrics: [
      XCTClockMetric(), 
      XCTCPUMetric(),
      XCTStorageMetric(), 
      XCTMemoryMetric()
    ]
  ) {
    sut.check(guess: 100)
  }
}

Этот тест измеряет несколько показателей:

  • XCTClockMetric измеряет сколько времени прошло.
  • XCTCPUMetric отслеживает активность CPU, включая время CPU, циклы и количество инструкций.
  • XCTStorageMetric сообщает вам, сколько данных тестируемый код записывает в хранилище.
  • XCTMemoryMetric отслеживает объем используемой физической памяти.

Запустите тест, затем щелкните на значок, который появляется рядом с началом завершающего замыкания measure(), чтобы просмотреть статистику. Вы можете изменить выбранную метрику рядом с Metric.

 

Щелкните Set Baseline, чтобы установить контрольное время. Снова запустите тест производительности и просмотрите результат - он может быть лучше или хуже базового. Кнопка «Edit» позволяет сбросить Baseline (точку отсчета) до этого нового результата.
Baseline сохраняются для каждой конфигурации устройства, поэтому вы можете выполнять один и тот же тест на нескольких разных устройствах. Каждый может поддерживать разные базовые показатели в зависимости от скорости процессора, памяти и т. д.

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

Включение покрытия кода

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

Чтобы включить покрытие кода, отредактируйте действие Test и установите флажок Gather coverage for для во вкладке Options:

Запустите все тесты с помощью Command-U, затем откройте Report navigator (навигатор отчетов) с помощью Command-9. Выберите Coverage под верхним элементом в этом списке:

Щелкните на раскрывающий треугольник, чтобы просмотреть список функций и закрытий в BullsEyeGame.swift:

Прокрутите до getRandomNumber(completion:), чтобы увидеть, что покрытие составляет 95,0%.

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

Аннотации покрытия показывают, сколько раз тест попадает в каждый раздел кода. Не вызванные разделы выделяются красным цветом.

Нужно ли достигать 100% покрытия?

Насколько сильно вы должны стремиться к 100% покрытию кода? Просто погуглите «100% покрытие unit-тестами», и вы найдете ряд аргументов за и против этого, а также споры по поводу самого определения «100% покрытия». Аргументы против этого говорят, что последние 10–15% не стоят затраченных усилий. Аргументы в пользу этого говорят, что последние 10–15% являются наиболее важными, потому что их очень сложно протестировать. Гугл считает, что «трудно тестировать то, что имеет плохой дизайн», то есть высказывает убедительные аргументы в пользу того, что непроверяемый код является признаком более глубоких проблем дизайна.

Оригинал статьи

Содержание