An iOS network mocking DSL library with minimal setup required.
The goal of this library is to provide a very simple way to mock the network calls your app performs, in a very unobtrusive way. As an example, you could setup what each UI test executes by using something similar to the following snippet:
let configurator = MockURLResponderConfigurator(scheme: "https", host: "api.myawesomeapp.com")
configurator.respond(to: "/path/to/resource", method: "GET")
.with(body: "{ \"json\": 123 }")
.always()
let application = XCUIApplication()
application.launchArguments = ["-MyCustomArgument"] + configurator.arguments + configuratorTwo.arguments
application.launch()
There are two separate components to MockURLResponder.
- MockURLResponder
- MockURLResponderTestAPI
The first one provides the mocking capabilities on the target that requires to mock network calls. The second one, will provide a DSL to generate launch arguments the first one consumes in order to configure the responses. Notice that this framework depends on the first one, so both frameworks will have to be embedded into the test targets.
Usually, this will mean that you embed MockURLResponder
into your app's target, and MockURLResponderTestAPI
onto your UI tests targets.
Embed into your apps target the MockURLResponder pod dependency:
pod 'MockURLResponder', :git => 'https://github.com/joseprl89/MockURLResponder'
And into your test target the MockURLResponderTestAPI pod:
pod 'MockURLResponderTestAPI', :git => 'https://github.com/joseprl89/MockURLResponder'
The basic setup of Carthage can be found here. For this specific project, add to your Cartfile the following line:
github "joseprl89/MockURLResponder"
This will compile the components described above:
- MockURLResponder
- MockURLResponderTestAPI
Once you have the built frameworks, drag and drop them into the targets that are going to use them.
If you encounter the image not found in the app target, go to Project
> App Target
> General
tab and add the framework in the Embedded binaries
box.
This is related to this issue
Follow the instructions here. In the sample app, it meant adding to the Build phases
a bash script running:
/usr/local/bin/carthage copy-frameworks
Using as input:
$(PROJECT_DIR)/Carthage/Build/iOS/MockURLResponderTestAPI.framework
$(PROJECT_DIR)/Carthage/Build/iOS/MockURLResponder.framework
This is related to this issue
The framework is split into two separate components to ensure that we can decouple our subject under test (SUT) from our test code which injects the expected responses.
The subject under test has a very slim integration with MockURLResponder, which is basically reduced to execute:
MockURLResponder.setUp()
This will will read the ProcessInfo launchArguments to configure the mocked responses, and then register a URLProtocol
in the shared URLSession
that will intercept network calls and respond as configured.
You can customise the behaviour of the system when finding a call that has not been mocked by setting MockURLResponder.Configuration.mockingBehaviour
. The values it can take are:
- preventNonMockedNetworkCalls: The default value. Upon finding a network call that hasn't been mocked,
fatalError()s
your application. Consider it a strict mock instead of a partial mock. - dropNonMockedNetworkCalls: Drops the network call and returns an error.
- allowNonMockedNetworkCalls: Allows non mocked calls to hit the network. Tread carefully, since this makes your tests slower and unreliable.
If you are using a custom url session, then make sure to add the MockURLProtocol class in the list of url protocols used by your custom URLSession. This can be done as:
var customConfiguration = URLSessionConfiguration()
// your setup
// ...
customConfiguration.protocolClasses = [MockURLProtocol.self]
URLSession(configuration: customConfiguration)
As mentioned earlier, the test code will inject into the SUT the mocked responses via its launch arguments. This allows to communicate the setup of our mocks painlessly from the XCUITests.
Namely, the integration starts by creating an instance (or more) of MockURLResponderConfigurator
:
let configurator = MockURLResponderConfigurator(scheme: "https", host: "www.google.com")
Once the configurator for a host is created, you can configure responses to one or more resources by using the respond method, which returns a response builder that can be further customised:
configurator.respond(to: "/", method: "GET")
.with(body: "Mock URL Responder is great!")
.always()
The builder allows to customise in detail the behaviour of the response. This is explained in detail in the Building a response.
Notice that as a last step of the response customisation there must always be a call to how many times the call must be mocked. This can be either of:
once()
times(N)
always()
This will allow you to customise a sequence of multiple responses for the same resource.
Finally, you have to pass this configuration into the SUT. This is done by injecting the arguments it generates via the XCUIApplication
launchArguments. As an advanced example, the following code would register multiple configurators that have been setup separately, plus custom arguments your app may be using:
let application = XCUIApplication()
application.launchArguments = ["-MyCustomArgument"] + configurator.arguments + configuratorTwo.arguments
application.launch()
Since the components of the framework are decoupled, they will allow to be used from within unit tests as well. The usage is quite similar to that of UITests, only it doesn't rely on the app delegate to setup the MockURLResponder and does it manually instead.
override func tearDown() {
MockURLResponder.tearDown()
}
func test_mocksSingleCall() {
let configurator = MockURLResponderConfigurator(scheme: "https", host: "www.w3.org")
configurator.respond(to: "/path", method: "GET")
.with(body: body)
.once()
MockURLResponder.setUp(with: configurator.arguments)
XCTAssertEqual(get("https://www.w3.org/path?q=query#fragment"), body)
}
Open the workspace at the root of this repo and run the unit tests in either of the schemes.
We follow Semantic Versioning. Currently we are on a pre v1.0 meaning that breaking changes could be added from one minor to another. To avoid confusion, we will bump patches when performing non breaking changes and minor when we perform breaking changes.
E.g. 0.1.0
is compatible with 0.1.1
, but 0.1.0
may break when upgrading to 0.2.0
.
We started the development of this framework with the goal to provide a dependency with the smallest footprint possible (in terms of size and maintainability), while allowing to easily decouple your project from it.
This would allow consumers to build on top of this framework their own testing solution rather than tightly coupling themselves to a 3rd party solution.
- Hannah Paulson for bringing the idea and helping me kickstart it.
- Adam Borek for adding CocoaPods support and kickstarting its use in production.