«Мочим» объекты с помощью 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


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

    Share post

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 3

      +1
      Рекомендую также ознакомиться с OHHTTPStubs
      https://github.com/AliSoftware/OHHTTPStubs
        +1
           func testGetSourceUrl() {
                let mock = MockUrlSessionProtocol()
                let urlStr  = "http://riis.com"
                let url  = URL(string:urlStr)!
                
                // Arrange
                stub(mock) { (mock) in
                    mock.getSourceUrl(apiUrl: urlStr).thenReturn(url)
                }
                
                // 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)
            }
        

        Такими тестами вы проводите не юнит тестирование класса UrlSession, а проверяете фреймворк Cuckoo на предмет сможет ли он замокать метод или нет.

        Правильное юнит тестирование было бы в том случае, если бы вы замокали класс URLSession и заинжектили его в UrlSession и далее проверяли бы логику написанного вами класса UrlSession.
          0

          Абсолютно согласен.

        Only users with full accounts can post comments. Log in, please.