2017/12/03 - go to articles list
Simple 5 points guide to leverage your unit testing into completely new level.
There are a lot going on lately in Swift world in general, and the trend did not miss testing part of development. While good testing practices are spreading, the need for tools designed to aid that process also grows. I have a pleasure to be a part of a team working on one of the solutions called SwiftyMocky.
When talking of unit testing and testing in general, there is a whole set of special purpose objects, called Test Doubles. In this article I will only briefly go through them, as it is needed to understand whole concept behind SwiftyMocky.
The whole story behind Test Doubles is quite simple. The, for lack of better word, classic way of testing often relies on state verification. We create initial state for sut and its dependencies, and then verify, whether state after performing test matches our expectations. Still, not in every test case we are testing sut, we can use real implementations as its dependencies.
Good example are database operations, or network calls. We don't really want to make real calls there, the knowledge that sut tried to do them is usually enough to assure validity. Another good example (from Martin Fowler post) is sending email - hard to verify from test perspective.
That's where our special case objects come into picture. Based on the vocabulary proposed by Gerard Meszaros (which i personally found pretty useful) I would split them into following categories:
For the rest of article we will focus on Mock, as it handles most of the stuff needed for proper testing.
Please note, that while vocabulary above strictly distinct between test doubles types, in most tools Mock is extended, with combined functionality of Stub and Spy. In SwiftyMocky we do the same, so whenever we refer to Mock, we mean object that also provides both of Stub and Spy features.
While idea of having Mock objects is quite clear, and benefits cannot be overstated, that leaves us with one problem. The mock implementations have to be written, which can become quite an overhead in big projects, with proper layer separation.
In languages that have proper reflections, there are numbers of libraries and frameworks that can create Mock in runtime, allowing you to choose one, fitting best your style of testing.
As Swift does not support reflections (yet?), other approaches are needed. There are bunch of solutions that are partially manual (when you have to implement part of the mock manual), and a few that uses meta-programming to generate complete mock implementation.
SwiftyMocky falls into second category (originally proposed by Przemysław Wośko). Whole concept is based on Sourcery (written by Krzysztof Zabłocki) and utilizes meta-programming concept.
First step is scanning sources, and checking for types, that could be subject of mocking. In SwiftyMocky it is done by either annotations, or adopting AutoMockable protocol.
Second step is generation of swift file, containing all mock's implementations, adopting from variety of Mock protocols (like Mock and StaticMock to name few). That file can be added to test target, allowing to use all generated classes.
In step three we could write tests, using whole set of handy methods for classes adopting Mock protocols, allowing:
Worth to notice is that with SwiftyMocky you can focus on step 3. Just write the tests and let the library handle rest of boilerplate stuff :)
Automatic mocks generation:
This greatly reduces time needed to setup tests. And its much easier to adopt to changes, as the mock implementations could be updated on the go.
Let's consider simple protocol.
//sourcery: AutoMockable
protocol UserStorageType {
func surname(for name: String) -> String
func storeUser(name: String, surname: String)
}
It will result in something like:
// MARK: - UserStorageType
class UserStorageTypeMock: UserStorageType, Mock {
func surname(for name: String) -> String {
// ...
}
func storeUser(name: String, surname: String) {
// ...
}
// ... mock internals
Please note, that for now only protocols are subject of Mock generation. We believe that interfacing is the proper way of layer separation and setting up dependencies in Swift. However, mocking classes are possible in the future (but not without limitations)
Easy stubbing:
One of the most important things for our mock implementation, if it's gonna be anything more than dummy, is a way to specify method return values. In typical plain stub we just hardcode value, but its not very flexible.
SwiftyMocky provides nice way to specify return values, both with easy syntax and flexibility. Please consider mock from above, that declares method surname(for name: String) -> String
:
SwiftyMocky allows us to do something like:
let mock = UserStorageTypeMock()
// For all calls with name Johny, we should return Bravo
Given(mock, .surname(for: .value("Johny"), willReturn: "Bravo"))
// For all other calls, regardless of value, we return Kowalsky
Given(mock, .surname(for: .any, willReturn: "Kowalsky"))
XCTAssertEqual(mock.surname(for: "Johny"), "Bravo")
XCTAssertEqual(mock.surname(for: "Mathew"), "Kowalsky")
XCTAssertEqual(mock.surname(for: "Joanna"), "Kowalsky")
Please note .
at the beginning of the method, as it strictly refers to:
Autocomplete:
We utilize as much power of autocomplete as possible, to aid process of writing tests. When performing Given, Verify, Perform on mock, type .
to get list of all methods declared by mocked protocol, that fits particular case. All of that is type-safe.
That is the situation, where one image is worth thousand words:
We don't force user into remembering additional stuff. And there is no longer a need for error prone "String" identifiers (like OCMock does).
Easy spying:
Stubbing is often not enough, and so we prepared a way to verify, whether a method was called (and how many times). Syntax is consistent with the one proposed in Given:
// inject mock to sut. Every time sut saves user data, it should trigger storage storeUser method
sut.usersStorage = mockStorage
sut.saveUser(name: "Johny", surname: "Bravo")
sut.saveUser(name: "Johny", surname: "Cage")
sut.saveUser(name: "Jon", surname: "Snow")
// check if Jon Snow was stored at least one time
Verify(mockStorage, .storeUser(name: .value("Jon"), surname: .value("Snow")))
// storeUser method should be triggered 3 times in total, regardless of attributes values
Verify(mockStorage, 3, .storeUser(name: .any, surname: .any))
// storeUser method should be triggered 2 times with name Johny
Verify(mockStorage, 2, .storeUser(name: .value("Johny"), surname: .any))
Flexibility:
Wherever it makes sense, we wrap method attributes into Parameter
enum. It allows to specify if we care about explicit value, or not:
All the cases could be mixed together, giving quite nice flexibility for stubbing and spying.
In the moment I'm writing that, version 2.0 is on its way, adding .matching(Type -> Bool) case to Parameter, providing even more flexibility.
Generics:
When I'm writing that, it seems that SwiftyMocky is the only Swift tool that supports generics. (Or at least both generics and mock generation).
//sourcery: AutoMockable
protocol ProtocolWithGenericMethods {
func methodWithGeneric<T>(lhs: T, rhs: T) -> Bool
}
We can use it in tests like following:
let mock = ProtocolWithGenericMethodsMock()
// For generics - you have to use .any(ValueType.Type) to avoid ambiguity
Given(mock, .methodWithGeneric(lhs: .any(Int.self), rhs: .any(Int.self), willReturn: false))
Given(mock, .methodWithGeneric(lhs: .any(String.self), rhs: .any(String.self), willReturn: true))
// In that case it is enough to specify type for only one element, so the type inference could do the rest
Given(mock, .methodWithGeneric(lhs: .value(1), rhs: .any, willReturn: true))
XCTAssertEqual(mock.methodWithGeneric(lhs: 1, rhs: 0), true)
XCTAssertEqual(mock.methodWithGeneric(lhs: 0, rhs: 1), false)
XCTAssertEqual(mock.methodWithGeneric(lhs: "a", rhs: "b"), true)
// Same applies to verify - specify type to avoid ambiguity
Verify(mock, 2, .methodWithGeneric(lhs: .any(Int.self), rhs: .any(Int.self)))
Verify(mock, 1, .methodWithGeneric(lhs: .any(String.self), rhs: .any(String.self)))
SwiftyMocky is available both by CocoaPods and Carthage, and additional steps required to run it differs a bit, based on dependency manager used. Full instructions are available here.
The simplest approach would be to use CocoaPods, as it handles getting Sourcery as well (in case of carthage you would have to do it yourself).
While I'm writing that, Sourcery is at version 0.9.0, compiled for Swift 4.0.0. It often leads to problems for users of Xcode 9.1, so along with SwiftyMocky comes script to override Sourcery in such a cases (check here)
Whole setup is done in yml config file. We could specify multiple locations for source files, both as list of files or folders. It usually looks something like below:
sources: # locations to scan
- ./ExampleApp
- ./ExampleAppTests
templates: # location of SwiftyMocky templates in Pods directory
- ./Pods/SwiftyMocky/Sources/Templates
output:
./ExampleApp # here Mock.generated.swift will be placed
args: # additional arguments
testable: # assure @testable imports added
- ExampleApp
import: # assure all external imports for mocks
- RxSwift
- RxBlocking
excludedSwiftLintRules: # for lint users
- force_cast
- function_body_length
- line_length
- vertical_whitespace
Then, to trigger generation, Sourcery is invoked with specified config. While it often looks something like that:
Pods/Sourcery/bin/Sourcery.app/Contents/MacOS/Sourcery --config mocky.yml
It could be wrapped in Rakefile, allowing to do just rake mock
:
# Rakefile
task :mock do
sh "Pods/Sourcery/bin/Sourcery.app/Contents/MacOS/Sourcery --config mocky.yml"
end
As mocks generation takes some time, we see no value in adding it to build run script phases, but we found it quite convenient to use Xcode behaviors and key bindings to regenerate mocks in project.
While there were several good tools for obj-c, Swift was still lacking some proper solutions. SwiftyMocky tries to address that, providing automatic mock generation and easy to get and use syntax, utilizing as much power of auto-complete as possible.
It is still a new thing, getting more mature overtime, yet bigger changes are still possible. Good thing about that is it still have velocity required to grow, providing new features and fixing current.
If I have to name why you should give it a try, I would say: