«Мочим» объекты с помощью Cuckoo
Пост написан по мотивам статьи 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
Спасибо за внимание.