- How Can Swift Language Features Improve Testability?
- Permission was granted to the original author, Jon Reid
- The Nuggets translation Project
- Translator: steinliber
- Proofreader: Edison-Hsu Graning
I know how to write testable C++ and Objective-C, but what does Swift do about it?
The characteristics and overall feel of a programming language can have a huge impact on how we present our code. I’m sure most of you already know this, because you started Swift early and are already ahead of me, and I’m more than happy to catch up with you. Swift features like new toys! But how do these features affect testability?
Write testable code
Full disclosure: The book link below is a side link, and if you buy any of them, I can earn a certain percentage, at no extra cost to you.
The biggest challenge of unit testing is writing testable code. This usually means learning how to code all over again for the first time! Working Effectively with Legacy Code provides many tips to help you write Code without thinking about testability. These techniques such as’ subclasses and method overloading ‘work well for legacy code, so I started using them in TDD implementations as well.
But one day, I asked myself, ‘Why do I keep writing legacy code?’
In other words, these techniques for dealing with legacy code are a stopgap measure. Isn’t there a way to improve testability without resorting to workarounds?
So I found Dependency Injection. One of the purposes of dependency injection is to provide test-complete control over the code it is testing.
Being based on different language features makes dependency injection much easier. Constructor injection is a better form of DI. Swift’s default parameter values make constructor injection easier. (But what if you have a Swift closure property? And it has a default closure, right? See below!)
Review: Validation of the Marvel API in Objective-C
I first learned about unit testing and TDD while writing C++. When I switched to Objective-C, it was a clean stream! Both production and test code are becoming more readable and easier to write.
Now I’ll redo my (sadly unfinished) TDD sample app, this time in Swift. Marvel Browser will be a simple app for exploring comic characters in the Marvel Universe.
Most of these already know how to exchange information with the Marvel API. This started with Spike Solution in Objective-C. In order to turn Spike into TDD compliant code, I had to deal with two things that would make unit testing tricky.
- The time stamp
- The MD5 hash
Instead of trying to solve everything in advance, I first tried subclassing and method overloading. That is, I bound the scope of these two things to the method, and then created a special subclass that only overloads the two methods.
This is a technique for dealing with legacy code, but it’s still a good way to start. The trick is not here.
How do we design something that can be replaced? I thought about using the policy pattern, but I decided to use code blocks instead. I’m going to turn these blocks into properties. This is the idea that opens the door to property injection.
How can we provide a default code block? Of course, I can do this in the initializer. But this would make the initializer all over the place, and I used lazy attributes – instead of delaying their initialization, I moved them out of the initializer.
Test time-dependent code
Some of the original objective-C code implementations bothered me when I started using Swift. To keep the code simple, the timestamp is implemented as an inert property that records the time of the first access. Multiple calls to this instance yield the same result. I tried to hide this by using factory mode.
J. B. Rainsberger has an excellent article on this issue. This article is really about a more general issue: getting your level of abstraction right, but this case is time-dependent code. In Beyond Mock Objects, he describes an example that requires different instances at one stage:
I find that a little strange, too. I’ve simplified dependencies in one way and made them more complex in another: the client must initialize a new controller for each request, or in other words, the controller has request scope. That sounds wrong.
I encourage you to research this article. Basically, instead of having a method in the problem to determine the timestamp, we can simply pass the timestamp as an argument. This is a classic use of method injection.
Method injection can be implemented in Objective-C using cascading methods. The interface should be clear:
- (NSString *)URLParameters;
- (NSString *)URLParametersWithTimestamp:(NSString *)timestamp;
Copy the code
The first method calls the second, providing a default value:
- (NSString *)URLParameters
{
return [self URLParametersWithTimestamp:[self timestamp]];
}
Copy the code
Swift makes it easier. We can simply use the default parameter values instead of using the cascade method.
func urlParameters(
timestamp: String = MarvelAuthentication.timestamp(),
/* more to come here */) -> String
Copy the code
One complication is that Swift does not allow us to call another instance method to get the default value. So if you can, implement it as a type method. (If not, we can always rely on cascading.)
Swift default property value
When I use attribute injection in Objective-C, I usually set the default value for the attribute. We can create small properties in the initializer. But sometimes you don’t want the code in your initializer to be program independent. Or sometimes it’s not small – it’s a block of code. In those cases, I’ll use the lazy attribute usage in Objective-C.
Here is my code block for calculating the MD5 hash:
- (NSString *(^)(NSString *))calculateMD5 { if (! _calculateMD5) { _calculateMD5 = ^(NSString *str){ /* Actual body goes here */ }; } return _calculateMD5; }Copy the code
But Swift allows us to set the default property value where the property is defined. This is a closure property with a default value:
var md5: (String) -> String = { str in
/* Actual body goes here */
}
Copy the code
Wow, that’s so much easier!
Closure experiment
Here’s what my main test looks like right now, and it’s good:
func testUrlParameters_ShouldHaveTimestampPublicKeyAndHashedConcatenation() { sut.privateKey = "Private" sut.publicKey = "Public" sut.md5 = { str in return "MD5" + str + "MD5" } let params = sut.urlParameters(timestamp: "Timestamp") XCTAssertEqual(params, "&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5") }Copy the code
As you can see, I overwrite the CLOSURE of MD5 to keep the tests understandable. This test shows that the generated URL parameters are correct. You can see how hashing works.
But then I thought, why override a closure property? Why not pass in the MD5 algorithm as a parameter? If we make it the last argument, then we can use tail-closure syntax:
func testUrlParameters_ShouldHaveTimestampPublicKeyAndHashedConcatenation() { sut.privateKey = "Private" sut.publicKey = "Public" let params = sut.urlParameters(timestamp: "Timestamp") { str in return "MD5" + str + "MD5" } XCTAssertEqual(params, "&ts=Timestamp&apikey=Public&hash=MD5TimestampPrivatePublicMD5") }Copy the code
Is this code more readable? To be honest, I think it’s even gotten a little worse. I use blank lines to group my tests into Three A’s (Arrange, Act, Assert). I think this particular closure messes up my execution, and it also has no name, which makes it harder to understand what it stands for.
But I had to find out!
Testable Swift
Here are some of the things I’ve learned so far about Swift that make it easy to write testable and clean code
Default: Swfit’s defaults simplify many dependency injection techniques:
- Constructor injection: Use default parameters in the initializer
- Property injection: Use the default property values
- Method injection: Use default parameter values in any method
Closures: The consistent syntax of Swift closures can introduce seams in a variety of places.
- Closure properties
- Closure parameters
- Because the syntax hasn’t changed extensively, refactoring is easier
- Because functions are closures, you can extract closures wherever you want. Why do everything in one line?
I don’t want to abuse closures. The alternative is that there is always an appropriate level of abstraction waiting to be discovered and then applied to the policy pattern. But every gap is an opportunity to improve testability
I have still only scratched the surface of Swift. I learned from Joe Masilotti’s article Better Unit Testing with Swift that agreements offer great opportunities. But how do other language features affect testability? Like enumerations or generics? Follow me as I explore them, test driven Swift, Subscribe Today!
What Swift features have you used to improve testability? What features should I explore? Let me know in the comments below