One of the more challenging places to get test coverage on iOS is custom UIView subclasses that override -drawRect:. Let’s examine how we do this at Wealthfront and look at an example from our iOS application.
There are a number of great reasons to implement custom UIViews, for example we have a simple custom view to draw the border around our feed icons as it has to be different colors for separate accounts. This approach greatly reduces the number of image assets we require to render the feed.
For this example we’ll look at how we test the view that draws our line chart, specifically the line of the chart itself, WFLineChartView.
As test driven developers our natural inclination is to get unit tests coverage for the bit of code that actually does the drawing, i.e. -drawRect:. But therein lies the rub, how do you actually test something like -drawRect:?
Reverse Engineering -drawRect:
The first step is to understand how -drawRect: works so that our test can set up the appropriate scaffolding to evaluate if -drawRect: is functioning correctly. My first iteration looked a little something like this:
When I ran it, I was greeted with a host of error messages like this one:
That certainly looks ominous. Let’s give drawRect a valid graphics context to work with and squash that error.
Unfortunately we’re not really testing anything yet. All we’ve done is confirm that calling -drawRect: won’t bring down the tests in some future iOS release. There’s nothing to ensure that its actually drawing a line. To understand what kind of validation we might add, lets take a look at a simplified version of the drawRect implementation:
As you can see the implementation of -drawRect: simply iterates over a set of points and adds them to an instance of UIBezierPath. So our test should validate that these points are generating the right type of additions to the path. But what kind of additions are we looking for? LLDB to rescue:
It looks like UIBezierPath is composed of a simple object which details the type of event, as well as the control points for that event. In reality this is actually backed by a CGPath, and the description method prints CGPathElement objects in a readable format. One simple test would be to generate a set of known points for the graph to draw and then decompose the bezier path in to these components for validation. To accomplish this the test must be able to track all instances of UIBezierPath created during -drawRect: and store them for validation.
There are a number of ways to tell a given unit test what instances of UIBezierPath are created during an invocation of -drawRect:. For example:
- NSNotifications that get posted to the test by the view.
- Adding a delegate to the view that it can ask for an instance of UIBezierPath
- A shared constructor that is used to create new instances of UIBezierPath
For our custom views that implement -drawRect: we’ve chosen to use a shared constructor that can return an instance of UIBezierPath.
This constructor makes it easy for our unit tests to keep track of all the bezier paths created by the view by using OCMock to replace it during the test. This also allows the test to validate the number of paths that are used by the view, thereby preventing any extraneous allocations. As a bonus, you can optionally surround it with a #define to remove the shared constructor in release builds, thereby eliminating the unnecessary invocation of objc_msg_send.
Note: It is also possible to use OCMock to mock +[UIBezierPath alloc] to return a specific instance of UIBezierPath, however this method will also be invoked by anything else that happens to draw while the tests are running. Given that the tests run inside of a live app, in the simulator or on a device, this approach is very hazardous and quite brittle. Any change in application structure or behavior (like a long running animation or load event) could end up invoking the mock’s +alloc replacement resulting in undefined behavior.
Validating the UIBezierPath
Now that we’ve created a way for the test to track what paths are created and used in -drawRect: all we have left to do is validate that the path is constructed correctly. A word on test scope, I don’t particularly care whether the system can correctly draw the UIBezierPath I construct, it’s highly unlikely the user will be able to even use their device if that’s broken. Therefore, the test simply needs to make sure that the path is composed of the proper components.
To help in this endeavor I created a little class to pull apart the CGPathElements into something easier to use in a test. The primary purpose is to expose properties for important values and abstract the parsing process away from the tests, greatly reduce the effort of writing new tests for engineers who don’t want to spend time worrying about CoreFoundation APIs.
Now we simply validate the path that is returned, for example the sample path above could be validated like so:
To make our code a little cleaner, most of the actual validation work is done inside the WFCGPathElement class’s isEqual method. All of our tests use these helpful macros to create one line validation statements for the test. This also ensures that failures have a consistent message structure.
With this simple test as a scaffold it is much easier to write other tests that evaluate boundary conditions like whole vs. fractional positioning on retina vs. non-retina devices. These conditions can easily be enumerated in a small series of tests that generate predetermined data points for the view to draw. Of course, the more complex the drawing logic the more tests you’ll want to write.