Harnessing Code Generation to Increase Reliability & Productivity on iOS at Uber
January 11, 2018 / GlobalAt Uber, our Android engineers have been using annotation-supported code generation for a few years now. On iOS, we first looked into code generation in fall 2016 when we started work on our new rider app using the RIBs architecture. One of the tenents of our new mobile architecture—reliability at scale—drove us to investigate whether we could use code generation to enhance the reliability of our mobile client and improve the developer experience by removing the need to manually write code that could be automatically generated from existing data sources.
It is important to note, however, that code generation is not an all-purpose solution. If you are using code generation to solve a problem that might be better solved by making your code more generic, code generation will probably have a degradative effect on the health and maintainability of your codebase. However, for certain use cases, code generation is an effective way to increase the reliability of your application and boost developer productivity.
In this article, we discuss two common code generation use cases—generating embedded resource accessors and test mocks—to highlight how the technique can be used to make applications more reliable and engineers more productive.
Resource accessors
iOS does not have an equivalent to Android’s R class, a class that is code generated to represent an application’s static resources. All access to static resources like images or strings and their translations are often evaluated dynamically at runtime:
[cc lang=”swift” nowrap=”false”]
let image = UIImage(named: “background”)
let translation = NSLocalizedString(“email.unreadMessages”, comment: “You have %d unread messages”)
[/cc]
This has two main problems. First, code becomes more complex as both the returned image and localized strings are optional, and this optionality has to be handled safely in order to avoid crashes in case the image is missing from the resource bundle.
Secondly, there are no compile-time steps that would catch the accidental removal of any of these resources, adding a lot of overhead for engineers who have to write unit test to make sure that the expected resources actually exist.
Resource accessor code generation
We solved these two problems by creating tooling that would inspect our project and generate static structs containing all available resources.
For images, the tooling would run through the asset catalogs associated with each project target, find relevant images, and generate a static struct with non-null accessors for all the images. Continuous integration would run the tooling too, making sure that if anyone accidentally deleted an image from any of the asset catalogs, the revision would fail to build and the erroneous change would not land.
For localized strings, a similar struct would be constructed. Additionally, the tooling would recognize localized strings that require input variables and generate API that guarantees that the string is only accessed with the correct parameters.
Take for example, the following formatted string in a strings file:
[cc lang=”swift” nowrap=”false”]
// Unread message count.
unreadMessages = “You have %d unread messages”
[/cc]
would generate an API that would make sure that engineers use the correct integer type to format the string:
[cc lang=”swift” nowrap=”false”]
final public class EmailStrings {
// Returns a localized string for “Unread message count”.
//
// – parameter value: A value to format the string with.
// – returns: The localized string.
public static func unreadMessages(_ value: Int) -> String {
return StringLoader.formattedStringWithKey(“email.unreadMessages”, inBundle:
classBundle, inTableName: tableName, values: value)
}
}
[/cc]
These code generation tools will eventually also go both ways. They will be able to inspect all source code and make sure that every image and localized string that is included in a build is actually referenced in code, keeping us from shipping unnecessary bytes in our application bundle when refactoring removes references to these resources.
Mocks
Uber’s new application architecture (RIBs) extensively uses protocols to keep its various components decoupled and testable. We used this architecture for the first time in our new rider application and moved our primary language from Objective-C to Swift. Since Swift is a very static language, unit testing became problematic. Dynamic languages have good frameworks to build test mocks, stubs, or stand-ins by dynamically creating or modifying existing concrete classes.
With a static language like Swift, creating specialized test mocks that lets you count the number of times a specific function has been called (for example) requires you to manually create a class that conforms to that protocol. Creating these test mocks can result in a significant amount of code. Very often, all of this code needs to be manually modified when the original protocol is updated.
Needless to say, we were not very excited about the additional complexity of manually writing and maintaining mock implementations for each of our thousands of protocols.
Code generating mocks
The information required to generate mock classes already exists in the Swift protocol. For Uber’s use case, we set out to create tooling that would let engineers automatically generate test mocks for any protocol they wanted by simply annotating them.
Our tool will parse the abstract syntax tree of each Swift file in each target, find any protocols that have been annotated with a @CreateMock comment, and generate conforming classes for each of them.
Consider the following protocol:
[cc lang=”swift” lines=”5″ nowrap=”false”]
/// @CreateMock
protocol UserPresentable {
var listener: UserPresentableListener { get }
func update(withUserInformation userInformation: UserInformation) -> Bool
}
[/cc]
In this protocol, concrete presenter classes will conform to show user information and report user actions back to the their parent using a listener property, whose type is another protocol. Running the mock generation tool on a codebase that contains the above protocol will generate the following mock class:
[cc lang=”swift” lines=”16″ nowrap=”false”]
// MARK: – UserPresentableMock class
/// A UserPresentableMock class used for testing.
class UserPresentableMock: UserPresentable {
// Variables
var listener = UserPresentableListenerMock() {
didSet { listenerSetCallCount += 1 }
}
var listenerSetCallCount = 0
// Function Handlers
var updateHandler: ((_ withUserInformation: UserInformation) -> ())?
var updateCallCount: Int = 0
init() {
}
func update(withUserInformation userInformation: UserInformation) -> Bool {
updateCallCount += 1
if let updateHandler = updateHandler {
return updateHandler(items)
}
// Default return value
return true
}
}
[/cc]
Both the listener property and the update-function contain counters that can be used in tests to verify that the listener property setter or update function have been called a certain number of times.The generated class conforms to the UserPresentable protocol by implementing the properties and methods required by the protocol. As a result, it can be used as a stand-in for the concrete implementation in unit tests. Generated code has some interesting features, outlined below:
- An updateHandler property has been generated, enabling engineers to implement tests that verify the arguments with which the update-function is called.
- The listener property in the original protocol is non-optional. Mock generation is able to fulfill this contract by constructing an instance of another protocol mock that it has recursively generated.
- Similarly, a default return value for the update handler is generated. Mock generation can generate defaults for all primitives, protocols, model objects, as well as many Foundation and UIKit classes. For many unit tests, the return value does not matter as you are only testing whether certain functions were actually called. Having default return values make developers more productive because they no longer have to implement a handler for all the functions that are involved in tests.
The iOS codebase for our rider application alone incorporates around 1,500 of these generated mocks. Without our code generation tool, all of these would have to be written and maintained by hand, which would have made testing much more time-intensive. Auto-generated mocks have contributed a lot to the unit test coverage that we have today.
Moving forward
We hope that this article demonstrates that there is real value in spending engineering time writing tools for code generation.
In addition to the use cases discussed, we also utilize code generation to produce all of the REST endpoints and models used in both our iOS and Android applications. In fact, the code produced by these tools account for as much as 20 percent of all of our code in our iOS codebase.
Leveraged correctly, code generation can make your code more reliable and your engineers more productive. At Uber, this boils down to two major use-cases:
- Representing an existing resource in code. For example, our back end uses Thrift to describe our REST API, including request and response models. The service endpoints and models have to be represented in code, so you can either maintain these models by hand (and risk making mistakes), or you can auto-generate them.
- Increasing reliability. Our resource accessor code generation is a good example of this. Without code generation, potential mistakes accessing resources are raised at runtime, with code generation we get compile-time safety.
We built these code generation tools ourselves for a number of reasons, including that there weren’t many open source tools available at the time we started our effort. Today, there are some great open source tools to generate resource accessors, like SwiftGen. And Sourcery can help you with generic code generation needs.
If building tools and systems that simultaneously enhance developer productivity and code reliability appeals to you, consider applying for a role on our team!
Tuomas Artman is a software engineer on Uber’s Developer Experience team, based in San Francisco.
Posted by Thomas Artous
Related articles
Most popular
Shifting E2E Testing Left at Uber
Massachusetts Driver Benefits
Navigating the LLM Landscape: Uber’s Innovation with GenAI Gateway
Continuous deployment for large monorepos
Products
Company