This morning I ran into yet another example of where tests can help you catch simple mistakes. I’m working on creating a custom control that simulates an automotive style fuel gauge.

This involves rotating a needle layer to indicate the fuel level. However, I don’t want to needle to rotate across the bottom 1/4th of the circle (over the gas pump icons), only across the top 3/4ths.

This became a problem when I added animations. UIView.animateWithDuration always takes the shortest distance. So animating the needle from pointing to empty to having it point to full would result in the needle moving counter-clockwise across the bottom of the gauge. A real gauge would never do that.

So to fix this problem, I split the animation into 2 sections. When the distance to move the needle is equal-to-or-greater than 180 degrees, then I use 2 animations, each of which are less than 180 degrees. So I created a method to perform the 180 degree test as shown:

func angleInRadiansIsEqualToOrGreaterThan180Degrees(angle: CGFloat) -> Bool {

let isGreaterThan180degrees: Bool = angle >= CGFloat(M_PI_2) || angle <= CGFloat(-M_PI_2)

return isGreaterThan180degrees

}

Unfortunately, angles are expressed in radians, not degrees, and that’s where I made the mistake. My quick, mental conversion of degrees-to-radians was wrong; 180 degrees equals pi, not pi/2. This mistake didn’t cause the code to fail, it just didn’t work as desired (using 2-part animations for all angles greater than 90 degrees instead of 180).

This morning I was going back and adding unit tests to the code (blush: not using TDD since this is a spike to figure out how to do the animations). In doing so, I correctly calculated the number of radians, causing the new tests to fail:

func testAngleInRadiansIsEqualToOrGreaterThan180DegreesTrueFor180() {

let angle = convertDegreesToRadians(180)

XCTAssertTrue(fuelGaugeView.angleInRadiansIsEqualToOrGreaterThan180Degrees(CGFloat(angle)), "Angle is equal to 180, but reported false")

}

func testAngleInRadiansIsEqualToOrGreaterThan180DegreesFalseFor90() {

let angle = convertDegreesToRadians(97)

XCTAssertFalse(fuelGaugeView.angleInRadiansIsEqualToOrGreaterThan180Degrees(CGFloat(angle)), “Angle (97) is less than 180, but reported true”)

}

func convertDegreesToRadians(degrees: Float) -> Float {

let radians = Float(M_PI / 180) * degrees

return radians

}

This raises a couple interesting points:

- Beware of mental math; for example, converting degrees to radians in my head in this case. I was lucky, and the 2nd time got it right, so my tests exposed the problem. However, I could very easily have made the exact same mistake in my tests.
- Where possible, use explicit calculations in your tests (if not also in your code). In this example, I created a convertDegreesToRadians() method in my tests. Had there been a similar function available in math.h, I would have used it.

Note: I will be releasing this code to GitHub shortly. It will include all of the code for a framework containing a set of animated, automotive style gauges including Fuel gauge, Speedometer, and Tachometer. I’ll update this post with a link once it is available.