
Пост написан по мотивам статьи Mocking in Swift with Cuckoo by Godfrey Nolan
По долгу своей "службы" мобильным разработчиком, предстала передо мной задача: разобраться с созданием и использованием Моков для юнит-тестирования. Моим коллегой была рекомендована библиотека Cuckoo. Стал я с ней разбираться и вот что из этого вышло.
Документация
Прочитав документацию на гитхабе мне, к сожалению, не удалось "завести" Cuckoo в моем проекте. Через CocoaPods этот фреймворк был установлен, но вот с Run-скриптом возникли проблемы: предложенный пример не создавал файл GeneratedMocks.swift в папке с тестами, и я бы и не разобрался почему, если бы не нашел через гугл статью, которую упомянул в начале поста.
Итак, пройдем все этапы вместе и разберемся с некоторыми нюансами.
Тестовый проект
Естественно, нам нужен какой-нибудь проект в который мы подключим Cuckoo и напишем несколько тестов. Откройте Xcode, и создайте новый Single View Application: язык — Swift, обязательно поставьте галочку Include Unit Tests, имя проекта — UrlWithCuckoo.
Добавьте в проект новый Swift-файл и назовите его UrlSession.swift. Вот полный код:
import Foundation class UrlSession { var url:URL? var session:URLSession? var apiUrl:String? func getSourceUrl(apiUrl:String) -> URL { url = URL(string:apiUrl) return url! } func callApi(url:URL) -> String { session = URLSession() var outputdata:String = "" let task = session?.dataTask(with: url as URL) { (data, _, _) -> Void in if let data = data { outputdata = String(data: data, encoding: String.Encoding.utf8)! print(outputdata) } } task?.resume() return outputdata } }
Как видите, это простой класс с тремя свойствами и двумя методами. Именно для этого класса мы и будем создавать Мок.
Подключаем Cuckoo
Я использую в работе CocoaPods, поэтому для подключения Cuckoo добавлю в каталог с проектом Podfile такого вида:
platform :ios, '9.0' use_frameworks! target 'UrlWithCuckooTests' do pod 'Cuckoo' end
Естественно нужно запустить pod install в терминале из каталога с проектом, и после завершения установки открыть в Xcode UrlWithCuckoo.xcworkspace.
Следующим шагом добавляем Run-скрипт в Build Phases нашего "таргета" тестирования (нужно нажать "+" и выбрать "New Run Script Phase"):

Вот полный текст скрипта:
# Define output file; change "${PROJECT_NAME}Tests" to your test's root source folder, if it's not the default name OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift" echo "Generated Mocks File = ${OUTPUT_FILE}" # Define input directory; change "${PROJECT_NAME}" to your project's root source folder, if it's not the default name INPUT_DIR="./${PROJECT_NAME}" echo "Mocks Input Directory = ${INPUT_DIR}" # Generate mock files; include as many input files as you'd like to create mocks for ${PODS_ROOT}/Cuckoo/run generate --testable "${PROJECT_NAME}" \ --output "${OUTPUT_FILE}" \ "${INPUT_DIR}/UrlSession.swift"
Как видите, в комментариях в скрипте написано о необходимости заменить ${PROJECT_NAME} и ${PROJECT_NAME}Tests, но в нашем примере в этом нет необходимости.
Генерируем Мок(и)
Дальше нам нужно, чтоб этот скрипт сработал и создал в каталоге с тестами файл GeneratedMocks.swift, и просто сбилдить проект (Cmd+B) для этого недостаточно. Нужно сделать Build For -> Testing (Shift+Cmd+U):

Проверьте что в каталоге UrlWithCuckooTests появился файл GeneratedMocks.swift. Его (файл) также нужно добавить в сам проект: просто перетащите его из Finder в Xcode в UrlWithCuckooTests:

Наши Моки готовы, поговорим о некоторых нюансах.
1. Сложные файловые структуры
Если у вас в проекте присутствует нормальная файловая структура и файлы разложены по подпапкам, а не просто находятся в корневом каталоге, то в скрипт нужно внести некоторые корректировки.
Допустим вы используете в своем проекте MVP и вам нужен Мок для вью-контроллера модуля MainModule (он у вас в проекте, конечно же, лежит по адресу /Modules/MainModule/MainModuleViewController.swift). В этом случае вам нужно поменять последнюю строку в скрипте из нашего примера "${INPUT_DIR}/UrlSession.swift" на "${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift".
Также если вы хотите, чтоб файл GeneratedMocks.swift попадал не просто в корневой каталог тестов, а, например, в подпапку Modules, то вам нужно подкорректировать в скрипте вот эту строку: OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift".
2. Нужны Моки нескольких классов
Очень вероятно (ожидаемая вероятность — 99.9%), что вам понадобятся Моки нескольких классов. Их можно сделать просто перечислив в конце скрипта файлы из которых нужно сделать Моки, разделив их обратными слэшами:
"${INPUT_DIR}/UrlSession.swift" \ "${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift" \ "${INPUT_DIR}/MyAwesomeObject.swift"
3. Аннотации типов
В классах к которым вы создаете Моки у всех свойств должны быть аннотации типов. Если у вас есть что-то типа такого:
var someBoolVariable = false
То при генерации Мока вы получите ошибку:

И в файле GeneratedMocks.swift будет фигурировать __UnknownType:

К сожалению Cuckoo не умеет определять тип по значению по умолчанию, и в таком случае необходимо явно указывать тип свойства:
var someBoolVariable: Bool = false
Пишем тесты
Теперь напишем несколько простых тестов используя наш Мок. Откроем файл UrlWithCuckooTests.swift и удалим из него два метода, которые создаются по умолчанию: func testExample() и func testPerformanceExample(). Они нам не понадобятся. И, конечно, не забудьте:
import Cuckoo
1. Свойства
Сначала напишем тесты для свойств. Создаем новый метод:
func testVariables() { }
Инициализируем в нем наш Мок и пару дополнительных констант:
let mock = MockUrlSession() let urlStr = "http://habrahabr.ru" let url = URL(string:urlStr)!
Теперь нам нужно написать stub-ы для свойств:
// Arrange stub(mock) { (mock) in when(mock.url).get.thenReturn(url) } stub(mock) { (mock) in when(mock.session).get.thenReturn(URLSession()) } stub(mock) { (mock) in when(mock.apiUrl).get.thenReturn(urlStr) }
Stub — это что-то типа подмены возвращаемого результата. Грубо говоря, мы описываем что вернет свойство нашего Мока, когда мы к нему обратимся. Как видите, мы используем thenReturn, но можем использовать и then. Это даст возможность не только вернуть значение, но и выполнить дополнительные действия. Например, наш первый stub можно описать и вот так:
// Arrange stub(mock) { (mock) in when(mock.url).get.then { (_) -> URL? in // some actions here return url } }
И, собственно, проверки (на значения и на nil):
// Act and Assert XCTAssertEqual(mock.url?.absoluteString, urlStr) XCTAssertNotNil(mock.session) XCTAssertEqual(mock.apiUrl, urlStr) XCTAssertNotNil(verify(mock).url) XCTAssertNotNil(verify(mock).session) XCTAssertNotNil(verify(mock).apiUrl)
2. Методы
Теперь протестируем вызовы методов нашего Мока. Создадим два тестовых метода:
func testGetSourceUrl() { } func testCallApi() { }
В обоих методах также инициализируем наш Мок и вспомогательные константы:
let mock = MockUrlSession() let urlStr = "http://habrahabr.ru" let url = URL(string:urlStr)!
Также в методе testCallApi() добавим счетчик вызовов:
var callApiCount = 0
Дальше в обоих методах напишем stub-ы.
testGetSourceUrl():
// Arrange stub(mock) { (mock) in mock.getSourceUrl(apiUrl: urlStr).thenReturn(url) }
testCallApi():
// Arrange stub(mock) { mock in mock.callApi(url: equal(to: url, equalWhen: { $0 == $1 })).then { (_) -> String in callApiCount += 1 return "{'firstName': 'John','lastName': 'Smith'}" } }
Проверяем первый метод:
// Act and Assert XCTAssertEqual(mock.getSourceUrl(apiUrl: urlStr), url) XCTAssertNotEqual(mock.getSourceUrl(apiUrl: urlStr), URL(string:"http://google.com")) verify(mock, times(2)).getSourceUrl(apiUrl: urlStr)
(в последней строке мы проверяем, что метод вызывался два раза)
И второй:
// Act and Assert XCTAssertEqual(mock.callApi(url: url),"{'firstName': 'John','lastName': 'Smith'}") XCTAssertNotEqual(mock.callApi(url: url), "Something else") verify(mock, times(2)).callApi(url: equal(to: url, equalWhen: { $0 == $1 })) XCTAssertEqual(callApiCount, 2)
(тут мы тоже проверяем количество вызовов, причем двумя способами: с помощью verify и счетчика вызовов callApiCount, который мы объявляли ранее)
Запускаем тесты
После запуска проекта на тестирование (Cmd+U) мы увидим вот такую картину:

Все работает, отлично. :)
И напоследок
Ссылка на то что у нас в итоге получилось: https://github.com/ssuhanov/UrlWithCuckoo
Спасибо за внимание.