
Пост написан по мотивам статьи 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 Cuckoo1. Свойства
Сначала напишем тесты для свойств. Создаем новый метод:
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
Спасибо за внимание.
