Uncle Bob set off another firestorm with his blog post The Dark Path. Condemnation from the Swift programming community was, well, swift. How dare he insult our wonderful new language? Clearly he’s a n00b who hasn’t done enough Swift programming.
Except that he’s no n00b — not even close. As Mark Seemann said,
Perhaps you disagree with @unclebobmartin (at times I do), but remember: he's been programming all of YOUR LIFE. Don't presume him ignorant.
— Mark Seemann (@ploeh) January 14, 2017
I’m here to defend Uncle Bob’s post, as it relates to unit testing and TDD.
False: “You don’t need as many tests”
Let me call out a common response. It was even stated in Chris Eidhof’s calm and measured Types vs TDD: A Response. Chris argues that it’s not strong typing or TDD, but both-and. I agree. Uncle Bob does, too, if you read his follow-up Types and Tests.
But Chris’s article included a statement I disagree with. It’s something I heard from quite a few people, across Twitter and at work.
A type checker actually does testing for you. It’s not a replacement for TDD, but it allows you to completely get rid of a whole bunch of tests. For example, if you define a method foo that returns an Int, you can be sure it will only return Ints. Not Strings, not nil or null, not anything else. No need to write a test.
In my experience, I haven’t found this to be true.
This may surprise you, especially if you know my enthusiasm for TDD. Why wouldn’t I write tests guaranteeing the return type?
Because most of the time, the “assert” section of a unit test is an equality check. It’s a simple way to say, “These are the property values I expect.” Having the same type is an implicit detail.
In fact, all that matters is that the object’s notion of equality is satisfied. So it could even be a different type! Conceptually, we’re just comparing the object’s properties. “When I send this object a particular message, do I get the expected result?”
There are even times when testing for equality is over-specification. In some situations, we care only about one property, not all of them. Both OCHamcrest and Swift Hamcrest support hasProperty matching. Even without Hamcrest, it’s why I recommend that Swift mock objects shouldn’t test for equality, but for an arbitrary predicate.
Over-specified tests are fragile tests. I usually don’t think about testing for a particular type. I’m even watchful when testing using equality.
When I do write unit tests for types
Like any rule-of-thumb, there are exceptions to the rule. I can think of a common case when I do test for a particular type. When I write unit tests around push navigation, I check:
- Did the UINavigationController receive
pushViewController(_:animated:)with the expected type of view controller?
- Were the required settings made on that view controller?
That’s the only common scenario in which I test the type: when there’s some sort of hand-off to a framework, where the handed-off object is a subclass.
Curiously, in such scenarios, strong typing doesn’t reduce test scope! The framework method signature only knows about the superclass, but the actual object is a subclass.
But what about nil?
“What about nil arguments, though, or nil return values?”
What about them? If I need them, I specify them in tests. If I don’t, I don’t.
In Objective-C, nil was rarely ever a problem. Basically, it provides an automatic implementation of the Null Object Pattern.
Swift forces me to consider nil for every Optional. This is usually good: it reminds me, “Oh, I may need more tests here.”
But it doesn’t decrease the tests.
Swift’s strict typing slows down my TDD
Strict typing has noticeably slowed my TDD. This is in contrast to Objective-C’s hinted-but-still-duck typing.
Why? It makes it harder to substitute fakes. There’s no way to say, “You expect type X. But I’m only testing. Could you give this object a pass? It’ll handle all the messages you need.”
Swift’s current inability to do so works against easy testing.
Perhaps some folks who are clever with compilers can find a workaround, kind of like
@testable. Given Apple’s track record with unit testing support, we’ll need clever people from the community.
But I’m not holding my breath, and I can’t wait. Neither should you. Here’s my plea:
Swift makes TDD harder. Don’t let that stop you from doing TDD.
The benefits of TDD are so strong, it’s worth pressing through the learning curve. Even when a particular language makes that curve harder. (Hey, I learned TDD in C++.)
Don’t give up. It’s worth it.
Both-and… but it’s not additive with Swift
I’ve been doing TDD for some time now. As far as I can tell, Swift’s strict typing hasn’t reduced the number of tests I write. Not by a single test.
Like Chris Eidhof, and even like Uncle Bob, I appreciate type-checking. I value early feedback from the compiler. But don’t ever mistake “it satisfies the compiler” for “it meets my requirements”. Type-checking can ensure that you’re getting the right type. Ah, but unit testing. Unit testing can ensure that those types are the right values.
So yeah, it’s both-and. The problem is, one is interfering with the other. It’s unit tests that are non-negotiable. I do hope Swift might evolve to be more tolerant of simple testing.
Agree? Disagree? Please leave your comments below.