Как стать автором
Обновить

«Мочим» объекты с помощью Cuckoo

Время на прочтение6 мин
Количество просмотров6.7K

Пост написан по мотивам статьи 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


Спасибо за внимание.

Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+10
Комментарии3

Публикации

Истории

Работа

Ближайшие события