Post written based on the article Mocking in Swift with Cuckoo by Godfrey Nolan
Due to my “service” mobile developer, the task appeared to me: to deal with the creation and use of Mocks for unit testing. My colleague recommended the Cuckoo library. I began to deal with it and this is what came of it.
After reading the githaba documentation, I unfortunately could not get Cuckoo in my project. This framework was installed via CocoaPods, but problems arose with the Run script: the proposed example did not create the GeneratedMocks.swift
file in the test folder, and I would not understand why if I hadn’t found the article I mentioned at the beginning of the post .
So, let's go through all the stages together and deal with some of the nuances.
Naturally, we need some project in which we will connect Cuckoo and write some tests. Open Xcode, and create a new Single View Application: language - Swift, be sure to check Include Unit Tests
, the project name is UrlWithCuckoo
.
Add a new Swift file to the project and name it UrlSession.swift
. Here is the full code:
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 } }
As you can see, this is a simple class with three properties and two methods. It is for this class that we will create Mok.
I use CocoaPods in my work, so to connect, Cuckoo will add to the directory with the Podfile project of this type:
platform :ios, '9.0' use_frameworks! target 'UrlWithCuckooTests' do pod 'Cuckoo' end
Naturally, you need to run pod install
in the terminal from the project directory, and after the installation is complete open in Xcode UrlWithCuckoo.xcworkspace
.
The next step is to add the Run script to the Build Phases of our testing target (you need to press "+" and select "New Run Script Phase"):
Here is the full text of the script:
# 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"
As you can see, the comments in the script say about the need to replace the ${PROJECT_NAME}
and ${PROJECT_NAME}Tests
, but in our example this is not necessary.
Next, we need this script to work and create the file GeneratedMocks.swift
in the test directory, and just knocking down the project ( Cmd+B
) is not enough for this. You need to make a Build For -> Testing ( Shift+Cmd+U
):
Verify that the file GeneratedMocks.swift
appears in the UrlWithCuckooTests
directory. You also need to add his (file) to the project itself: just drag it from the Finder to Xcode in UrlWithCuckooTests
:
Our moki are ready, let's talk about some nuances.
If you have a normal file structure in your project and the files are arranged in subfolders, and not just located in the root directory, then you need to make some adjustments to the script.
Suppose you are using MVP in your project and you need a Mock for the MainModule
module controller MainModule
(it is in your project, of course, located at /Modules/MainModule/MainModuleViewController.swift
). In this case, you need to change the last line in the script from our example "${INPUT_DIR}/UrlSession.swift"
to "${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift"
.
Also, if you want the GeneratedMocks.swift
file to get not just to the root directory of tests, but, for example, to the Modules
subfolder, then you need to correct this line in the script: OUTPUT_FILE="./${PROJECT_NAME}Tests/GeneratedMocks.swift"
.
It is very likely (the expected probability is 99.9%) that you will need the Moki of several classes. They can be done simply by listing at the end of the script the files from which Moki should be made, separating them with backslashes:
"${INPUT_DIR}/UrlSession.swift" \ "${INPUT_DIR}/Modules/MainModule/MainModuleViewController.swift" \ "${INPUT_DIR}/MyAwesomeObject.swift"
In the classes to which you create the Mocks, all properties must have type annotations. If you have something like this:
var someBoolVariable = false
Then when you generate mock you will get an error:
And in the file GeneratedMocks.swift
will be __UnknownType
:
Unfortunately, Cuckoo does not know how to define a type by default, and in this case, you must explicitly specify the type of the property:
var someBoolVariable: Bool = false
Now we will write some simple tests using our Mok. Open the UrlWithCuckooTests.swift
file and remove from it two methods that are created by default: func testExample()
and func testPerformanceExample()
. We do not need them. And, of course, do not forget:
import Cuckoo
First we write tests for properties. Create a new method:
func testVariables() { }
We initialize our Mok and a couple of additional constants in it:
let mock = MockUrlSession() let urlStr = "http://habrahabr.ru" let url = URL(string:urlStr)!
Now we need to write stubs for the properties:
// 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 is something like substitution of the returned result. Roughly speaking, we describe what will return the property of our Moka, when we turn to it. As you can see, we use thenReturn
, but we can use then
. This will make it possible not only to return the value, but also to perform additional actions. For example, our first stub can be described like this:
// Arrange stub(mock) { (mock) in when(mock.url).get.then { (_) -> URL? in // some actions here return url } }
And, actually, checks (for values and for 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)
Now we will test calls of methods of our Moka. Create two test methods:
func testGetSourceUrl() { } func testCallApi() { }
In both methods we also initialize our Mock and auxiliary constants:
let mock = MockUrlSession() let urlStr = "http://habrahabr.ru" let url = URL(string:urlStr)!
Also in the testCallApi()
method, add a call counter:
var callApiCount = 0
Further in both methods we will write 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'}" } }
Check the first method:
// 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)
(in the last line we check that the method was called twice)
And the second:
// 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)
(here we also check the number of calls, and in two ways: using verify
and callApiCount
call callApiCount
, which we announced earlier)
After launching the project for testing ( Cmd+U
), we will see the following picture:
Everything works perfectly. :)
Link to what we finally got: https://github.com/ssuhanov/UrlWithCuckoo
Thanks for attention.
Source: https://habr.com/ru/post/322572/
All Articles