Goodbye Moya & Alamofire. Simplify Your Dependencies
In the last company I worked, we decided to ditch them. Perhaps you should too.
Back in the early days in the history of iOS (before iOS 4), many developers came to the realisation that the out-of-the-box networking code from the OS was a little short on functionality and AFNetworking
came to the rescue with a whole host of gems, and nice to haves. When Swift was introduced Alamofire followed in the same vein to provide all the tools and conveniences for elegant network code, and resulted in massive adoption industry-wide. Then Moya took one step further to add another layer of syntactic sugar to give a clean interface to writing endpoints, which I must add, I really like a lot. I am absolutely over-simplifying this but I do recommend reading the documentation and articles about it and make up your own mind.
But today (Jan 2022), iOS has come a long way. As developers we now have URLSession
, data tasks and Combine, Apple’s take on reactive programming, at our disposal. The premise for the existence of Alamofire was that the state of the code needed too much repetition between projects and thus a 3rd party framework was born, but this is (sort of) no longer the case.
In this article, I show, in the context of an existing project, how to replace those dependancies with both Combine and Async/Await, and explain an approach to improve the reliability of unit and UI tests by mocking HTTP responses using URLSession
, and without introducing any 3rd party dependency.
The source code of the project for this article is here, the commits made here.
This repo uses Tuist for managing the Xcode project generation and schemes. Use version 1.52 and not the latest release.
See the previous articles in this series where I describe the process of modularisation of a simple 4-scene monolithic project. The project now has 8 modules as nested folders under Features.
Recommended reading before continuing, here’s a brief introduction to Combine
Introduction
I started the initial demo app in 2019 and took advantage of Moya using the relatively new Swift Package Manager for the dependency management, in place of CocoaPods which I’d been using for years. Although I like the more Swift approach to this, over using Ruby in CocoaPods, there were 2 things I didn’t like so much about this integration: Swift packages are resolved when Xcode opens a project which can mean it’s slow to get started on the project, and second: The number of additional SPMs that are loaded with Moya.
At one point when I did a minor version update, those dependencies ballooned (3–4 more were added). It was a temporary state and a fix was pushed in a matter of days but that set my mind on just removing this extra baggage for what is only some syntax sugar that can be easily reproduced with a few classes and structures. And since I’d been working for more than a year on a company app that only used URLSession
the decision was clear. It’s got to go!
Combine Publishers
A quick look at the README
and you can see that the app has a modular architecture, with clear separations of concern. The Network module is where the most of the changes are going to happen, thus the first step is to run tuist focus NetworkKitExampleApp NetworkKitTests
. That builds the Xcode project with the NetworkKit module, its test target and the example app. Where previously a search request function relied on passing a completion block, Combine provides a more elegant approach of a publisher, where commands can be chained together in a far more readable manner.
With the introduction of Combine, one of the most useful additions to Apple’s URLSession is a dataTaskPublisher(for: urlRequest)
. I use that here, but also try to maintain a similar method signature. Moya was used to define this endpoint as an enumeration which to prevent typographical errors, and this will be maintained to an extent.
Instead of returning a concrete object, a publisher can be defined using a generic type: AnyPublisher<Data, Error>
.Therefore the SearchService
protocol changes to:
public func search(identifier:Int) -> AnyPublisher<Data, Error>
Keep That Search Alive
Since the search function only has a single integer as a parameter it requires a way to make the URLRequest, but as this was one of the features of the MoyaProvider
that I am removing. Fortunately removing Moya is trivial for this use case. Here’s the factory method for the URLRequest
as an extension to the endpoint definition:
A recommended approach to the Combine data task publisher implementation is to use tryMap
operator and catch the possible errors with guard statements, and using eraseToAnyPublisher
to make the return type a generic publisher which matches the function signature.
The error handling is a simple enumeration as follows:
Finally, the search function now looks like:
Testing
I update the test to have a sink for the publisher and a switch case on the completion property which is a type returned by Combine.
One thing I noticed is that is wrong with this is that it is hitting the network, although it can run offline when URLSession caches the response. Ideally all the testing should be run offline in a reliable and consistent manner. I am going to need to mock the response here.
Mocking
Over the years, I have come across various solutions to mocking HTTPS requests such as OHHTTPStubs
, but Paul Hudson brought my attention to URLProtocolMock. By creating a session configuration and setting the URLProtocol
class as to return data loaded locally for specific URLs, in an instance of URLSession
, it is simple to return stubbed responses for any URL you need to test against without hitting the network. the real advantage of this is that it doesn’t introduce any external 3rd party dependencies.
I insert this code into the start of the test case to configure the URLSession
and the test passes for any integer value for the identifier
There is one problem with it though and this related to the JSON file in the unit test target. The module struct is defined in the Tuist/ProjectDescriptionHelpers/Project+Templates.swift
and only resource paths are explicitly defined for each of the framework and the example app. I had to extend this structure to include a path for the unit testing target in the same way because if I just modified the project and template definition for the general cases, that would imply that all test targets would have to find a JSON file in a similar file path respect to the module name, and as such it would fail to generate the Xcode project file.
The updated module structure is here
Improving Test Coverage
One thing that is known about the Pokemon API is that there are only a limited number of Pokemon characters and as such only integer values below 900 are valid. Any integer ≥ 900 will return a HTTP 401. A test should cover this functionality and an empty stub should be added accordingly. For this case the network module should throw a notFound
response error.
The problem with this is that as it is now, the URLProtocolMock
only returns HTTP 200 status codes. What if it was possible to mock the full response and not just the data? That could look something like this:
struct MockResponse {
let response: URLResponse
let url: URL
let data: Data?
}
The URLProtocolMock
class can have a dictionary like so:
static var testResponses = [URL: MockResponse]()
Next I add a factory for the mock URLSession instance.
By using this factory method the previous test is updated with:
let session = MockSessionFactory.make(url: url,
data: data,
statusCode: 200)
This now gives me the possibility to easily test the 401 error.
Testing Express
The above sections I described how to run automated tests by first opening a project in a module with the testing target, and then running it (Command+U), or just individually running tests in Xcode. But when there are many modules, opening each one is a tedious process. Wouldn’t it be nice if there was a simple way to do this from the command line, and not have to specify every parameter for Xcodebuild… Well, guess what? Yes, there is. Just use tuist test
.
This command can run all the tests in one go, or you can specify a scheme to run. This is a huge time saver. More here.
Note: There was one issue that caused a problem with tuist test. The swift lint build phase script as it fails if relative paths are used because the code is copied to a cached folder when running the tests, and this will break the paths. The cache path is of the form
~/.tuist/Cache/Projects/<Project hash>
The swiftlint.sh script in the scripts folder needs to be updated and fix the $ROOT_PATH. A simple fix is to pass in the path of the Xcode project as a command line argument to the script like this:
TargetAction.post(path: "scripts/swiftlint.sh", arguments: ["$SRCROOT", "$TARGETNAME"], name: "SwiftLint")
The Example App And Main App Target
The network module example app needs a similar update to get that working as well with the exception for the implementation in the receiveValue closure is this:
Where the queue
is defined as DispatchQueue.main
With the example app now running and the tests working, I change the focus to the main Pokedex app. The code in the search function in the DataProviderExtension
needs to be updated to work with Combine. The changes required are almost identical to those added to the example app.
The main issue now is that the UI tests fail. This is due to the changes done in the network module while removing Moya.
Mocks For UI Testing
When running a UI test you generally want to do so in a fast and repeatable way, so that your tests will always pass and be reliable, not at the mercy of the service is connects to, and that the UI elements you are testing are populated with the data and objects that you need to verify. Thus, those network calls should be mocked in some way. Moya provided a simple way to do that, so now I need to insert a fix.
When running UI tests, there is a simple mechanism to inject strings to change behaviour by adding launch arguments.
app.launchArguments += ["UITesting"]
The application can then detect this arguments using this:
public static var uiTesting: Bool {
let arguments = ProcessInfo.processInfo.arguments
return arguments.contains("UITesting")
}
The Configuration structure has this role, and the network service should be modified accordingly.
A short addition in the search function fixes the UI testing by returning a Just publisher with mock data and finishes.
if Configuration.uiTesting {
return Just(loadMockData())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
Await No More
Announced at the 2021 WWDC, async/await is the concurrency API that many developers have been waiting years for. This approach reducesand simplifies the syntax required for writing complex asynchronous code. In the PokemonSearchService I add 2 async functions to the protocol definition. Note that these functions can throw an error, instead of returning a tuple with data and an error object.
public protocol SearchService: AnyObject {
func search(identifier: Int) async throws -> Data?
func performRequest(urlRequest: URLRequest)
async throws -> Data?
}
And the implementation is:
This is much cleaner and easier to follow. The search function is now:
To cover this code with unit tests I add a test for each case of success and failure. But first I add a small refactor for building the mock search service:
The the success test code is this:
What is nice about this is that it is no longer required to add an expectation and a wait, since XCTestCase
has been extended to support async tests directly.
The error case is very easy to validate. This really shows how compact and concise asynchronous requests can now be, and as a result less error prone.
Pokedex Async
The final step is to update the data provider to support the new Async API code, but I didn’t want to over write the Combine implementation so I add a switch that can be set in LaunchArguments
to enable the Async API, leaving Combine as the default search method.
In the Configuration source file in the Common module I add
public static var asyncTesting: Bool {
let arguments = ProcessInfo.processInfo.arguments
return arguments.contains("AsyncTesting")
}
Then in the DataProviderExtension of the Pokedex app I use the suggestion that John Sundell made here by wrapping the async code in a Task:
Next, I add a UI test to cover this case by adding 2 launch arguments:
app.launchArguments += ["UITesting", “AsyncTesting"]
To improve the reliability of the UI tests, I disable the view animations in the AppDelegate
.
I update the makeSchemes function in Project+Templates (run tuist edit
to see this) where a scheme has launch arguments inserted
[LaunchArgument(name: "AsyncTesting", isEnabled: true)]
In in the Pokedex project the scheme looks like this:
Recap
Migrating away from Moya and its other dependencies can be simple as it was here, and if you are starting a new project I suggest you don’t use it at all, and just stick with the native iOS APIs in URLSession.
The code presented covers asynchronous network requests in a separate module using both Combine and Async/Await, with detailed examples on how to write unit tests for each case, UI tests for the integrated app.
Testing was improved with additional coverage and the project validated by running tuist test
.
Additionally there are 2 custom schemes which by default have launch arguments set to enable each of UITesting with the stubs and the async API so that anyone running the project can validate these without changing the source code.
This project is using Tuist version 1.52, but the latest version at the time of writing is 2.5.0. In the next article I will go through the steps of updating the project and template syntax and start using the new Dependencies.swift approach which can take advantage of a cache and binary XCFrameworks.
The final source for this article is here.
Please follow to know about the upcoming articles.
Further Reading
I you are interested in gaining a deeper insight into either Combine or Async/Await both of these books are excellent:
Combine: Asynchronous Programming with Swift
References
The official Swift documentation on concurrency is a must read.
John Sundell’s articles provide detailed insights into a wide range of aspects of Swift
Connecting Async/Await with other Code
Paul Hudson’s website is another prime source for all topics Swift related.