Robert Martin brought condemnation from the Swift community with his post The Dark Path. How dare he insult our wonderful new language? Clearly, he’s a newbie who hasn’t done enough Swift programming.
Except that he’s no newbie—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
Let’s examine the controversial post as it relates to Swift—focusing on 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. Robert Martin 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 hear from quite a few people.
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 Int. Not String, not nil or null, not anything else. No need to write a test.
In my experience, I haven’t found this to be true.
Swift's strict typing hasn't reduced the number of tests I write.
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.
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?” What people forget about object-oriented programming is that types are just a helpful means to an end. We don't actually care what type something is. What we care about is: does this object do the things we want? (Types happen to be convenient for this, but are not the whole story.)
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.
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 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. It provides an automatic implementation of the Null Object Pattern.
Swift forces me to consider nil for every Optional type. 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 Robert Martin, 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.
- Type Safety, 100% Coverage, and Robert Martin: Who’s Right?
- Static Analysis: Will It Free You from TDD?
Agree? Disagree? Please leave your comments below.
Professionals use the tools that ensure correctness and automate where possible. There is nothing that can be done in one language that cannot be done in the other. Having a compiler perform tests instead of my creating my own I consider a win. As you have pointed out this doesn’t obviate the need for tests, but it does remove some of them. I have also found that I write code that is either filtering optionals or performing logic and the resultant code is easier to test. Sort of a analog to constructing objects or performing logic.
You and I are not new to this game Jon and it can be jarring to learn a new way of doing things. It has been for me. During these transitions I have noticed that some paradigms are a better fit for how I naturally approach problems, but what I find a better fit is a worse fit for others. Moving from Java to objective-C and being able to message to nil objects has never seemed safe. But I have worked with those who leverage that feature and produce professional code. Reactive frameworks have much less magic in Swift and functional programming is in theory more testable. Its a strange new world and I have found swift to be very nice, if only they could make the compiler faster.
Great to hear from you, Todd! You’re right, transition is hard. We have to let go of old habits, and acquire new ones. And I do like a lot of the new stuff, particularly enumerations with associated values. (I still need to get to the point where I call map or flatMap, but I’m sure my coworkers will call that out in some PR review.)
As Swift use grows, some things will happen naturally. The compiler will speed up. Unnecessarily rebuilt code will diminish. Apple will do these things because they fit with the company mindset.
But other things won’t happen unless we drag the language there. Apple’s track record of supporting unit testing has been historically poor. They show little interest. I mainly wrote this article to call out the idea that strict typing reduces tests, that somehow it reduces the need. But it’s also my way of waving the testing flag to Apple, and to all contributors to Swift evolution: “Please make testing easier!”
As a programmer, I make lots of mistakes. I always have, even back on the trusty old IBM 1130. If there’s a way to Do It Wrong, I’ve done it.
But one thing I almost never find in my ObjC/ObjC++ code is type errors. In years of work on a moderately large app and a bundle of prototypes, I think I’ve seen perhaps three or four crashes that involved type errors.
Is this not the general experience?
Yes, not returning nil reduces tests, but it’s good practice anyway; it simplifies code. Sentinels and null objects are familiar patterns now; we’ve got that covered.
We know (post Coplien) that TDD doesn’t literally test everything; we can’t hope to do that. In practice, I seldom test type correctness even when the underlying structures or functions might conceivably permit type errors, because those errors just don’t happen.
Thank you for sharing your experience, Mark!
Hi Mark, I do feel that I spend a lot of my tie trying to determine what objects actually. This seems to be particularly troublesome with libraries that are generic. I am counting the bugs found during development when I miscast an object or check to see if a method is implemented that will never be implemented because someone changed the underlying type. Also I believe that type checking provides for better refactoring and compiler optimizations. The optimizations I am sure about, the more information a compiler has the better its optimizations can be. And the number of times bugs that we get from items being nil? Swift has convinced me that nil checking and type checking are two sides of the same coin.
Perhaps taking “can I eliminate this test through introducing types?” would make an interesting avenue to explore, along the lines of Yaron Minsky’s advice to “make invalid states unrepresentable” and Ken Fox’s “more typing, less testing” TDD using types demo (https://spin.atomicobject.com/2014/12/09/typed-language-tdd-part1/).
These are great articles! Thanks for the reference.