This is an example project setup managing dependencies with property wrappers code while allowing fast writing of unit tests.
- uses @Injected from Resolver by @hmlongco
- generates mocks using Mockingbird
You could argue that the beauty of Swift is related to the compiler taking care of so many things. But this also causes problems, like boilerplate code.
- long init's medium blog
Swift5.1 Takes Dependency Injection to the Next Level
- writing a mock for a class or protocol while upscaling the team can be challenging
So in this repo I experiment and explain what I struggled with when trying to use injection frameworks and writing independent tests. My struggles are:
- Injection in Swift is not supported by the language. Swinject tries to give an alternative but it is very bulky and can cause in my opinion over complicated code
- Injection relies on a
Resolver
singleton that is shared in the test, causing tests cases to depend on each other.
With this repo I want to explore if Resolver and Mockingbird might help me overcome this problem.
I start by explaining how to generate mocks and then explain the tests.
In the discussion a test is written with a Controller
that has a private service property. As we do not want to do a network call in a test case in the test the service should not actually perform the request, in other words we would like to mock it out.
If you like you could read this nice article explaining difference between Mocks and Stubs.
The nice guys @Bird helped me out quickly with a workaround to build without xcode. detailed explanation here
- To install the generator, you’ll want to download the prebuilt binary and either check that into your project, or make it a build step in your CI (see the Makefile for an example).
- run bash script that generates xcode project and removes when done
./Scripts/workaroundMockingbird.sh
I asume you know about property wrappers that where introduced in swift 5.1. If not head over to an article from NSHipster explaining what it is and how it came to be.
In the test target I added some cases to showcase:
- what is @Injected and how does it use a Resolver?
- when generating mocks how to mock a protocol and do some tests on the mocked objects?
@Injected is a property wrapper that when compiled sets the value of the property to whatever is registered in the Resolver for the type of the property.
Unfortunately this means your tests depend on the state of Resolver.main
. And just like it is no good to have your tests depend on a database or network state this dependency on Resolver.main
is bad. One test could register something for a type and another test could overwrite that easily. But there is a way to overcome this problem by using a cached resolver.
A cached resolver uses the factories of a type only once until you reset it.
func testResolvedViaInjectedPropertyWrapper() {
// Have to use the resolver that is cached so we can reset it in every test
Resolver.defaultScope = Resolver.cached
// Set the static main resolver again. That is the resolver that will be used on injection
Resolver.main = Resolver()
// register some classes
}
So far we setup test with a Resolver
to be able to pass our mocks when @Injected
is compiled. But what does that mean?
Say we have a Controller
that has a property that sets the service
via @Injected
. As an extra difficulty the NetworkService
complies to protocol NetworkServiceable
. The way this would work was not obvious to me at first.
In our tests we can instantiate a mock according to Mockingbird like so
let service: NetworkServiceableMock = mock(NetworkServiceable.self)
Resolver.register { service }.implementing(NetworkServiceable.self)
Resolver.register { service }
registers a factory {...}
to the resolver to be used by @Injected to instantiate the service on the property of our Controller object. The .implementing(NetworkServiceable.self)
part makes it also work for a protocol and not only properties with type NetworkService
.
Let's have a look at the controller
final class Controller {
@Injected private var service: NetworkServiceable
func loadData() { service.fetch() }
}
The Controller
has a private
property that will handle the service request. In the test we are going to write we would like to test:
Is the
fetch
function called on the service when we callloadData
on theController
?
The mock that was registered for the service can be stubbed thanks to the generated code of mockingbird. Go a head and run the test and have a look at the full test.