Туториал по модульному тестированию iOS и UI тестированию

05 сентября 2018

Создавайте лучшие приложения, используя модульное тестирование iOS!
Написание тестов - процесс не очень привлекательный, но, принимая во внимание тот факт, что тесты не дадут вашему приложению превратиться в барахло, полное багов, все же процесс этот – необходимый. Если вы сейчас читаете «Туториал по модульному тестированиө iOS и UI тестированию», вы наверняка уже знаете, что крайне важно писать тесты для своего кода и пользовательского интерфейса, но еще не до конца уверены, как проводить тестирование в среде разработки Xcode.
Возможно, у вас уже есть «действующее» приложение, но нет подходящих для него тестов, а вы хотите иметь возможность тестировать любое изменение в процессе совершенствования вашего приложения. Или, может быть, у вас есть написанные готовые тесты, но вы не уверены, правильные ли они. А возможно, вы сейчас работаете над своим приложением и хотите тестировать его в процессе разработки.
Эта статья «Туториал по модульному тестированию iOS и UI тестированию» поможет вам разобраться, как использовать навигатор Xcode тестирования, чтобы тестировать модель приложения и асинхронные методы, как имитировать обмен данными с библиотекой или объектами системы, используя имитирующие объекты и объекты–заглушки, как проверить UI (пользовательский интерфейс) и его эффективность и как использовать инструментальное средство покрытия кода. Заодно вы пополните свой словарный запас терминами, которые используют виртуозы тестирования, и к концу туториала вы уже будете уверенно внедрять зависимости в свою тестируемую систему (System Under Test (SUT)!

Тестирование, тестирование…

Что тестировать?

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

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

Сначала о главном: лучшие практики (методы) тестирования

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

  • Быстрота: Тесты должны выполняться быстро, чтобы людям хотелось их выполнять.
  • Независимость/Изолированность: Тесты должны полностью игнорировать друг друга. Результаты выполнения одного теста не должны быть входными данными для другого.
  • Повторяемость: Вы должны получать одинаковые результаты каждый раз при запуске теста, не зависимо от среды выполнения. Провайдеры/источники внешних данных и вопросы параллелизма могут вызвать перемежающиеся сбои.
  • Очевидность (Самодостоверность): Результатом выполнения теста должно быть булево значение. Тест либо прошел, либо не прошел, и это должно быть легко понятно любому разработчику.
  • Своевременность: В идеале тесты должны быть написаны непосредственно перед тем, как вы напишете код программного продукта, который они должны проверить.

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

Начинаем

Скачайте, разархивируйте, откройте и изучите проекты для начинающих (starter projects) BullsEye and HalfTunes.
BullsEye основан на примере приложения в “Новичкам в iOS” (iOS Apprentice). Я переместил логику игры в класс BullsEyeGame и добавил стиль альтернативной игры.
В нижнем правом углу есть сегментированный элемент управления, который позволяет пользователю выбирать стиль игры: либо Slide, когда мы перемещаем ползунок, чтобы подойти как можно ближе к заданной величине, переместив ползунок, либо Type, когда мы угадываем позицию ползунка. Действие элемента управления также сохраняет выбор стиля игры пользователя как пользователь по умолчанию.

HalfTunes  - пример приложения из нашего туториала «URLSession Tutorial», адаптированного под Swift 4. Пользователи могут запрашивать песни на iTunes API , затем загружать их и проигрывать фрагменты этих песен.

Итак, начнем тестирование!

Модульное тестирование в Xcode

Создание таргета модульного теста

Навигатор Xcode Test Navigator  позволяет легко работать с тестами; он понадобиться вам, чтобы создать цели теста и запустить тесты в вашем приложении.
Откройте проект BullsEye и выберите Command-6, чтобы открыть навигатор тестирования.
Нажмите символ «+» в нижнем левом углу, затем выберите в меню New Unit Test Target… :

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


Шаблон импортирует фреймворк XCTest и определяет подкласс BullsEyeTests - XCTestCase, с setUp()tearDown() и примером методов тестирования.

Существует три способа запустить тестовый класс:

  • Product\Test или Command-U. Этот способ запускает фактически все тестовые классы.
  • Нажать кнопку с символом «стрелка» в тестовом навигаторе.
  • Нажать кнопку с символом «ромб» в столбце.

Вы также можете запустить индивидуальный тестовый метод, нажав на символ «ромб» либо в навигаторе, либо в столбце.
Попробуйте разные способы запуска тестирования, чтобы понять, сколько времени это занимает и как этот процесс вообще выглядит. Тесты выборочного контроля еще ничего не делают, поэтому они будут работать очень быстро!
Когда все тесты успешно завершены, «ромбики» станут зелеными, и на них появятся галочки. Нажмите на серый ромбик в конце testPerformanceExample(), чтобы открыть Performance Result (Результат производительности):

testPerformanceExample() вам будет не нужен, так что удалите его.

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

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

@testable import BullsEye

Это даст модульным тестам доступ в классы и методы в BullsEye.

В верхней части класса BullsEyeTests добавьте это свойство:

var gameUnderTest: BullsEyeGame!

Создайте и запустите новый объект BullsEyeGame в setUp(), после вызова super:

gameUnderTest = BullsEyeGame()
gameUnderTest.startNewGame()

Таким образом, вы создадите объект SUT (тестируемая система) на уровне класса, и все тесты в этом тестовом классе будут иметь доступ к свойствам и методам объекта тестируемой системы.
Здесь вы также вызываете метод игры startNewGame, который создает targetValue. Многие ваши тесты будут использовать targetValue, для того, чтобы убедиться, что игра правильно ведет счет. Пока не забыли, отпустите тестируемую систему в tearDown() до того как вызовете super:

gameUnderTest = nil

Заметка

Чтобы быть уверенным, что каждый тест начинается с нового листа, будет неплохо потренироваться создавать SUT (тестируемую систему) в setUp()  и отпускать ее в tearDown(). Чтобы продолжить дискуссию, советую ознакомиться с постом Джона Рейда на эту тему.

Теперь вы готовы к написанию вашего первого теста!

Заменить testExample() на следующий код:

// XCTAssert to test model
func testScoreIsComputed() {
  // 1. given
  let guess = gameUnderTest.targetValue + 5
  
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

Имя тестового метода всегда начинается с test, за которым идет описание того, что он проверяет.
Правильно форматировать тест на разделы given/when/then:
1. В разделе given установите любые необходимые значения: в данном примере вы задаете значение  guess, чтобы вы могли определить, насколько оно отличается от targetValue.
2. В разделе when выполните проверяемый код: вызовите gameUnderTest.check(_:).
3. В разделе then подтвердите ожидаемый результат сообщением (в данном случае, gameUnderTest.scoreRound равно 100 – 5), которое печатается при непрохождении теста.
Запустите тестирование, нажав на символ «ромб» в столбце или тестовом навигаторе. Приложение скомпилируется и запустится, символ «ромб» станет зеленым с галочкой!

Заметка

Чтобы ознакомиться с полным перечнем XCTestAssertions, нажмите XCTAssertEqual в коде, чтобы открыть XCTestAssertions.h, или перейдите на Apple’s Assertions Listed by Category.

Заметка

Структура теста Given-When-Then берет свое начало в BDD (разработка через реализацию поведения) и является термином удобным для клиента. Альтернативные названия такой системы Arrange-Act-Assert и Assemble-Activate-Assert.

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

В BullsEyeGame намеренно создана ошибка, поэтому теперь вы будете учиться искать эту ошибку. Чтобы увидеть ошибку в действии, переименуйте testScoreIsComputed на testScoreIsComputedWhenGuessGTTarget, затем выполните операцию Копировать-Вставить-Правка (copy-paste-edit), чтобы создать testScoreIsComputedWhenGuessLTTarget.
В этом тесте, вычитаем 5 из targetValue в разделе given. Все остальное оставляем без изменений:

func testScoreIsComputedWhenGuessLTTarget() {
  // 1. given
  let guess = gameUnderTest.targetValue - 5
  
  // 2. when
  _ = gameUnderTest.check(guess: guess)
  
  // 3. then
  XCTAssertEqual(gameUnderTest.scoreRound, 95, "Score computed from guess is wrong")
}

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

Запустите свой тест: он должен остановиться на строке XCTAssertEqual с Ошибкой тестирования (Сбой).
Изучите gameUnderTest и guess в консоле отладки:

guess –это targetValue - 5, но при этом scoreRound - 105, а не 95!
Чтобы продолжить изучение, воспользуйтесь обычным процессом отладки: установите место прерывания в разделе when и еще одно в BullsEyeGame.swift, в check(_:), где оно создает difference. Теперь заново запустите тест и пропустите блок let difference, чтобы изучить значение difference в приложении:

Проблема в том, что difference отрицательная, поэтому счет 100 – (-5); решением этой проблемы будет использование абсолютного значения разницы. В блоке check(_:), отмените правильную строчку и удалите неправильную.
Удалите два брейкпоинта и запустите тест заново, чтобы убедиться, что теперь все пройдет успешно.

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

Теперь, когда вы научились тестировать модели и устранять ошибки тестирования, давайте перейдем к использованию XCTestExpectation для тестирования сетевых операций.
Откройте проект  HalfTunes: он использует URLSession для запроса песни на iTunes API и загрузки некоторых фрагментов песен.
Предположим, вы хотите модифицировать его, чтобы использовать AlamoFire для сетевых операций. Чтобы убедиться, что ничего не сломается, вам нужно написать тесты для сетевых операций и запустить их до и после того, как вы измените код.
Методы URLSession асинхронны: они тут же возвращаются, но не заканчивают работать еще какое-то время. Чтобы тестировать асинхронные методы, вы используете XCTestExpectation, чтобы заставить ваш тест дождаться завершения выполнения асинхронной операции.

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

Выберете в +меню New Unit Test Target…  и назовите HalfTunesSlowTests. Импортируйте приложение HalfTunes сразу после  команды import:

@testable import HalfTunes

Все тесты этого класса будут использовать сеанс по умолчанию для отправки запросов серверу Приложения, поэтому объявите объект sessionUnderTest , создайте его в setUp() и отпустите в tearDown():

var sessionUnderTest: URLSession!

override func setUp() {
  super.setUp()
  sessionUnderTest = URLSession(configuration: URLSessionConfiguration.default)
}

override func tearDown() {
  sessionUnderTest = nil
  super.tearDown()
}

Замените testExample() на ваш асинхронный тест:

// Asynchronous test: success fast, failure slow
func testValidCallToiTunesGetsHTTPStatusCode200() {
  // given
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Status code: 200")
  
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, 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
  waitForExpectations(timeout: 5, handler: nil)
}

Этот тест необходим для того, чтобы убедиться, что отправленный корректный запрос на iTunes получит в ответ код состояния 200 – что означает, запрос обработан успешно. Большая часть кода та же самая, что вы бы написали в приложении, со следующими дополнительными строками:
1. expectation(_:)  возвращает объект XCTestExpectation, который вы храните в promise. Другие общепринятые названия для этого объекта - expectation and future. Параметр description описывает то, что должно произойти на ваш взгляд.
2. Чтобы соответсвовать description, вы вызываете promise.fulfill() при условии закрытия обработчика завершения асинхронного метода.
3. waitForExpectations(_:handler:) позволяет тесту работать до тех пор пока все ожидания не будут выполнены, или когда закончится период ожидания timeout , не важно что произойдет в первую очередь.

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

Ускоряем невыполнение тестирования

Невыполнение теста неприятно, но оно не должно длиться целую вечность. Давайте разберем способ, как быстро обнаружить, что тестирование не выполнено, сэкономив время, которое вы с большей пользой можете потратить, сидя в SwiftBook. :]

Для того, чтобы изменить тест так, чтобы асинхронная операция не удалась, просто удалите ‘s’ из “itunes” в URL (единый указатель ресурса):

let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")

Запустите тест: он не выполняется, но его работа занимает все время периода ожидания! Это происходит потому, что тест ожидает, что запрос будет обработан успешно, и именно здесь вы вызвали promise.fulfill(). Так как запрос не был обработан, тестирование заканчивается только тогда, когда истекает период ожидания.
Вы можете ускорить процесс невыполнения тестирования, заменив ожидание теста: вместо того, чтобы ждать пока запрос будет выполнен, дождитесь только того момента, когда будет вызван completion handler (обработчик) асинхронного метода. Это происходит сразу после того, как приложение получает ответ от сервера – либо ОК либо Ошибка, что соответствует ожиданию. После этого тест может проверить, выполнен ли запрос успешно.
Чтобы увидеть, как это работает в деле, создайте новый тест. Сначала исправьте этот тест, для этого отменив изменение в url (единый указатель ресурса), затем добавьте следующий тест к вашему классу:

// Asynchronous test: faster fail
func testCallToiTunesCompletes() {
  // given
  let url = URL(string: "https://itune.apple.com/search?media=music&entity=song&term=abba")
  // 1
  let promise = expectation(description: "Completion handler invoked")
  var statusCode: Int?
  var responseError: Error?
  
  // when
  let dataTask = sessionUnderTest.dataTask(with: url!) { data, response, error in
    statusCode = (response as? HTTPURLResponse)?.statusCode
    responseError = error
    // 2
    promise.fulfill()
  }
  dataTask.resume()
  // 3
  waitForExpectations(timeout: 5, handler: nil)
  
  // then
  XCTAssertNil(responseError)
  XCTAssertEqual(statusCode, 200)
}

Самое важное здесь то, что просто вход в completion handler (обработчик), уже удовлетворяет ожиданию теста, а это занимает всего секунду. Если запрос не выполнен, то и условие then не выполняется.
Запустите тест: теперь на невыполнение теста уйдет всего секунда, а тест не выполняется потому, что не выполняется запрос, а не потому, что у теста вышел период ожидания timeout.
Исправьте url, затем заново запустите тестирование, чтобы убедиться, что теперь он пройдет успешно.

«Фейковые» объекты и взаимодействия

Асинхронные тесты помогают вам убедиться в том, что ваш код формирует правильный сигнал на асинхронный API. Возможно, вы также захотите убедиться, что ваш код работает правильно, когда он получает сигнал от URLSession, или что он правильно обновляет UserDefaults или базу данных CloudKit.
Большинство приложений взаимодействуют с системой или объектами библиотеки – объектами, которые вы не можете контролировать – и тесты, которые взаимодействуют с этими объектами, могут быть медленными и невозможными для повторения, нарушающими два ПЕРВЫХ принципа. Вместо этого вы можете «подделать» эти взаимодействия, получив сигнал от стаба (stub) или обновив мок (mock). Применяйте «подделку», когда ваш код имеет зависимость от системы или объекта библиотеки – создайте фейковый объект, чтобы он сыграл эту роль и введите этот «фейк» в ваш код. В статье Джона Рейда «Внедрение зависимости» описывается несколько способов, как это сделать.

Фиктивный сигнал от стаба.

Этот тест поможет вам понять, что метод приложения updateSearchResults(_:)  правильно анализирует информацию, загруженную сессией, убедившись, что searchResults.count является верным. SUT - это контролер представления, и вы «подделываете» сессию при помощи стабов и некоторых, предварительно загруженных, данных.

Выберите в + Меню New Unit Test Target… и назовите его HalfTunesFakeTests. Импортируйте приложение HalfTunes сразу после строки import:

@testable import HalfTunes

Объявите SUT, создайте его в настройках setUp() и отпустите в  tearDown():

var controllerUnderTest: SearchViewController!

override func setUp() {
  super.setUp()
  controllerUnderTest = UIStoryboard(name: "Main", 
      bundle: nil).instantiateInitialViewController() as! SearchViewController!
}

override func tearDown() {
  controllerUnderTest = nil
  super.tearDown()
}

Заметка

SUT – это контроллер представления потому, что у HalfTunes проблема с массивным контроллером представления – вся работа выполняется в SearchViewController.swift. Перевод сетевого кода в отдельные модули частично решит проблему, а также упростит тестирование.

Далее, вам понадобится образцы данных JSON, которыми ваша фейковая сессия будет обеспечивать ваш тест. Будет достаточно нескольких данных, поэтому чтобы ограничить загруженные результаты iTunes добавьте  &limit=3 к строке URL (единый указатель ресурса):

https://itunes.apple.com/search?media=music&entity=song&term=abba&limit=3

Скопируйте этот URL (единый указатель ресурса) и вставьте в браузер. Загрузится файл, который будет называться 1.txt или как-то похоже. Предварительно просмотрите его, чтобы убедиться, что это файл JSON, затем переименуйте его в abbaData.json и добавьте файл к группе HalfTunesFakeTests .
Проект HalfTunes содержит вспомогательный файл DHURLSessionMock.swift. Он определяет простой протокол, который называется DHURLSession, а также методы (стабы), предназначенные для создания задачи данных с URL или с  URLRequest.
Он также определяет URLSessionMock, который соответствует этому протоколу, с инициализаторами, которые позволяют вам создать имитирующий объект URLSession с выбранной вами информацией, ответом и ошибкой.
Установите фиктивную информацию и ответ, и создайте имитирующий/фиктивный объект сессии, в setUp() после команды, которая создает SUT:

let testBundle = Bundle(for: type(of: self))
let path = testBundle.path(forResource: "abbaData", ofType: "json")
let data = try? Data(contentsOf: URL(fileURLWithPath: path!), options: .alwaysMapped)

let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
let urlResponse = HTTPURLResponse(url: url!, statusCode: 200, httpVersion: nil, headerFields: nil)

let sessionMock = URLSessionMock(data: data, response: urlResponse, error: nil)

В конце setUp(), введите фиктивную сессию в приложение в качестве свойства SUT:

controllerUnderTest.defaultSession = sessionMock

Заметка

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

Теперь вы готовы написать тест, который проверит, распознает ли вызов updateSearchResults(_:) фиктивную информацию.
Замените testExample() на следующее:

// Fake URLSession with DHURLSession protocol and stubs
func test_UpdateSearchResults_ParsesData() {
  // given
  let promise = expectation(description: "Status code: 200")
  
  // when
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 0, "searchResults should be empty before the data task runs")
  let url = URL(string: "https://itunes.apple.com/search?media=music&entity=song&term=abba")
  let dataTask = controllerUnderTest?.defaultSession.dataTask(with: url!) {
    data, response, error in
    // if HTTP request is successful, call updateSearchResults(_:) which parses the response data into Tracks
    if let error = error {
      print(error.localizedDescription)
    } else if let httpResponse = response as? HTTPURLResponse {
      if httpResponse.statusCode == 200 {
        promise.fulfill()
        self.controllerUnderTest?.updateSearchResults(data)
      }
    }
  }
  dataTask?.resume()
  waitForExpectations(timeout: 5, handler: nil)
  
  // then
  XCTAssertEqual(controllerUnderTest?.searchResults.count, 3, "Didn't parse 3 items from fake response")
}

Вам все равно придется писать этот тест асинхронным, потому что стаб «притворяется», что он представляет из себя асинхронный метод.
В разделе when - searchResults пуст, прежде чем запущена задача данных — это происходит потому, что вы создали совершенно новую SUT в setUp().
Фиктивные данные содержат JSON для трех объектов Track , поэтому в разделе then массив searchResults контроллера представления содержит в себе три элемента.
Запустите тест. Он должен успешно завершиться довольно быстро, потому как настоящее сетевое соединение отсутствует!

Имитирующее обновление для объекта мок

В предыдущем тесте использовался стаб для обеспечения ввода фиктивного объекта. Далее, вы будете использовать мок для того, чтобы проверить, корректно ли ваш код обновляет UserDefaults.
Заново откройте проект BullsEye. У приложения два стиля игры: пользователь либо перемещает ползунок, чтобы соответствовать искомой величине, либо угадывает искомое значение по позиции ползунка . Сегментированный элемент управления в нижнем правом углу переключает стиль игры и обновляет соответственно gameStyle пользователя по умолчанию.
Ваш следующий тест проверит, корректно ли обновляет ваше приложение gameStyle пользователя по умолчанию.
В навигаторе теста нажмите/кликните New Unit Test Target… и назовите его 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, тем самым мы получаем больше гибкости – например,  ваш тест проверяет, вызывается ли тест только один раз.
Объявите SUT и фиктивный объект в BullsEyeMockTests:

var controllerUnderTest: ViewController!
var mockUserDefaults: MockUserDefaults!

В setUp(), создайте SUT и фейковый объект, затем введите фейковый объект в качестве свойства SUT:

controllerUnderTest = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController!
mockUserDefaults = MockUserDefaults(suiteName: "testing")!
controllerUnderTest.defaults = mockUserDefaults

Отпустите SUT и фейковый объект в tearDown():

controllerUnderTest = nil
mockUserDefaults = nil

замените testExample() на следующее:

// Mock to test interaction with UserDefaults
func testGameStyleCanBeChanged() {
  // given
  let segmentedControl = UISegmentedControl()
  
  // when
  XCTAssertEqual(mockUserDefaults.gameStyleChanged, 0, "gameStyleChanged should be 0 before sendActions")
  segmentedControl.addTarget(controllerUnderTest, 
      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:) был точно вызван один раз.
Запустите тест, он завершится успешно.

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

В Xcode 7 было внедрено тестирование пользовательского интерфейса, который дает вам возможность создать тест пользовательского интерфейса, просто записав взаимодействия с пользовательским интерфейсом. Тестирование пользовательского интерфейса находит в приложении объекты пользовательского интерфейса с запросами, производит синтез события, и затем отправляет их к этим объектам. Программный интерфейс приложения (API) дает вам возможность изучить свойства и состояние объекта пользовательского интерфейса (UI) с целью сравнить их с ожидаемым состоянием.
В тестовом навигаторе проекта BullsEye, добавьте новую цель тестирования пользовательского интерфейса (UI Test Target). Убедитесь, что этот Таргет тестируется в BullsEye, затем примите имя пользователя BullsEyeUITests.
Добавьте это свойство на самом верху класса BullsEyeUITests:

var app: XCUIApplication!

В настройках setUp(), замените команду XCUIApplication().launch() на следующее:

app = XCUIApplication()
app.launch()

Смените имя testExample() на testGameStyleSwitch().
В testGameStyleSwitch() откройте новую строку и кликните на красную кнопку Записи (Record ) в самом низу окна редактора:

Когда приложение появится на симуляторе, нажмите Slide (бегунок) – сегмент переключателя стиля игры и верхняя метка label. Затем кликните кнопку Записи Xcode (Xcode Record button), чтобы остановить запись.
Теперь у вас есть следующие три строки в testGameStyleSwitch():

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

Если есть какие-либо другие команды, удалите их.
Строка 1 дублирует свойство, которое вы создали в Настройках setUp() и вам не нужно больше ничего нажимать, поэтому удалите также и первую строку и .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)
}

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

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

Из документации Appleтест производительности берет блок кода, который вы хотите оценить, и запускает его десять раз, выявляя при этом среднее время выполнения и стандартное отклонение запусков. Усреднение этих отдельных измерений имеет большое значение при запуске теста, они затем могут сравниваться с основными данными, и таким образом может определяться прошел ли тест успешно или нет.
Тест производительности написать очень легко: вы просто помещаете код, который вы хотите измерить, в заключительную часть метода measure().
Чтобы увидеть этот процесс в действии, заново откройте проект HalfTunes и в HalfTunesFakeTests, замените testPerformanceExample() на следующий тест:

// Performance 
func test_StartDownload_Performance() {
  let track = Track(name: "Waterloo", artist: "ABBA", 
      previewUrl: "http://a821.phobos.apple.com/us/r30/Music/d7/ba/ce/mzm.vsyjlsff.aac.p.m4a")
  measure {
    self.controllerUnderTest?.startDownload(track)
  }
}

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


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

Покрытие кода (Code Coverage)

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

Заметка

Запускать ли тест производительности в то время как покрытие кода запущено? В документации Apple сказано: Сбор данных о покрытии кода приводит к снижению производительности, что влияет на выполнение кода линейно, таким образом результаты производительности остаются сравнительными от одного запуска теста к другому, если он запущен. Однако, следует принимать во внимание, применять ли покрытие кода, когда вы критически оцениваете производительность программы в ваших тестах.

Чтобы включить покрытие кода, во вкладке Test отметьте галочкой Code Caverage:


Запустите все тесты (Command-U), затем откройте навигатор отчетов (Command-8). Выберите By Time, выберите верхний элемент в этом списке, затем выберите вкладку Coverage:

Кликните на треугольник раскрытия, чтобы увидеть список функций в SearchViewController.swift:

Наведите курсор мышки на голубую панель покрытия рядом с updateSearchResults(_:) и вы увидите, что процент покрытия равен 71.88%.
Кликните на кнопку со стрелочкой, чтобы открыть файл исходного кода, затем укажите месторасположение функции. Когда вы наведете курсор мышки на аннотации покрытия в правой боковой панели, участки кода подсветятся зеленым или красным цветом:

Аннотации покрытия показывают, сколько раз тест обращался к каждому участку кода; участки кода, которые не были вызваны, подсветятся красным цветом. Как и следовало ожидать, цикл FOR запускался 3 раза, но ничего не было выполнено на пути ошибок. Чтобы увеличить покрытие этой функции, вы можете скопировать abbaData.json, затем отредактировать его так, чтобы он вызывал другие ошибки – например, измените "results" на "result" для теста, который  проверяет print("Results key not found in dictionary").

100% покрытие?

На сколько яростно вы должны стремиться к 100% покрытию кода? Загуглите “100% покрытие модульного тестирования”, и вы найдете доводы за и против, а также дебаты по поводу вопроса, что же такое «100% покрытие». Доводы «против» утверждают, что последние 10-15% - не стоят наших усилий. Аргументы «за» говорят о том, что последние 10-15% - самые важные, т.к. их тяжело протестировать. Загуглите “сложность модульного тестирования из-за плохого проектирования», и вы найдете убедительные доводы в пользу того, что нетестируемый код – это признак более глубоких проблем проектирования. Дальнейшее изучение вопроса может привести к выводу, что Разработка, основанная на тестировании (Test Driven Development) и есть путь, по которому нужно следовать.

Источник статьи

Содержание