[This post is part of the series TDD Sample App: The Complete Collection …So Far]
In the Marvel Browser project, we created a Request Model to encapsulate the parameters to our FetchCharactersMarvelService. We used Constructor Injection to begin TDDing the network request. Now we want to verify translating the Request Model into URL parameters.
First, we want namePrefix in the Request Model to become the nameStartsWith URL parameter. Here’s a naive test:
There’s a problem: it tests for strict equality against an expected URL. What happens when we add more parameters? The test will fail, even though the new parameters aren’t relevant to this test!
We need to test only part of the URL argument. But how?
We could start by capturing the argument with the HCArgumentCaptor matcher from OCHamcrest:
Now we can examine urlArgument.query, which is the query string. We could search it for an expected substring:
But this doesn’t guard against accidental characters on either end of the string. No, it’s safest to use NSURLComponents to parse the URL into its parts:
OK, now we need to search the queryItems array. We can create an expected NSURLQueryItem and test that it’s present in the array. Let’s put all the pieces together:
Wow, this is getting way too long and complicated!
This verification code is getting longer and longer. The longer it gets, the less confidence I have. And it doesn’t even have any control flow statements! What would happen to my confidence if we needed loops or conditionals?
Thankfully, there’s a good way out: Make it some kind of assertion test helper. Then test the helper.
In fact, depending on the approach you take, you can even TDD it. But some approaches are better than others…
There are several ways to write assertion test helpers. Let’s start with the most common way, and why you shouldn’t use it.
I often see test helper methods that contain an assertion within the method. For example, here’s a test helper asserting that a URL contains a query:
This looks like a straightforward application of Extract Method, right? The problem is with the XCTAssertTrue. When it fails, what location will it report? It’ll report the line number of the XCTAssertTrue. This isn’t helpful, because we want the line number where the test method invoked the test helper.
As more tests use this test helper, the situation gets worse. The reporting from Xcode becomes confusing. You have to dig around to find the actual tests that failed.
Test failures should point to the test code, not to the assertion test helper. Avoid putting assertions within methods or functions.
XCTAssertTrue is able to report its own line number because it’s a preprocessor macro. It’s easy to see this and conclude that a good way to get the line number correct in an assertion test helper is to write their own macro.
Well, it depends.
Not long ago, Apple’s testing framework was OCUnit. In OCUnit, the assertion macros contained everything they needed within the macro. So it was common to copy this style:
Line number reporting works because the preprocessor expands the entire macro in-place, including the embedded XCTAssertEqual. But writing multiline macros is a pain. Each line has to have the backslash continuation character. There’s the strange-looking “do-while(0)” trick to make the compiler treat those multiple lines as a single expression. There are preprocessor tricks like this that old hands may remember, but are easy to get subtly wrong.
I’ve written before about avoiding preprocessor use. Let me add another reason: good luck trying to step through the code in a debugger.
A better approach is to minimize the macro, getting only what we need from the preprocessor: the file name and line number. And maybe self so you can identify the test class. Extract everything else to a function, passing these in as function arguments. This is the approach Apple finally adopted with XCTest.
Swift: Assertion methods are much better in Swift. You can treat the file name and line number as parameters with default values, which expand to the point-of-call. So there’s no need for macro magic! For more, see How to Make Specialized Test Assertions in Swift.
Instead of making a macro, it’s usually easier to create a method that contains everything but the assertion. Just return a boolean value. The method then becomes a predicate — converting its arguments into a single YES or NO value.
It’s easier to write tests for predicate methods. This also makes them easier to TDD.
The assertion then lives in the test body:
This is nice and concise. An assertion failure will report the correct line number. But what else will it report about the failure? Nothing.
You can improve this by having the assertion write out the actual URL:
Now a failure will yield the correct line, and it will also print the URL received.
But what if a predicate could do more than report YES or NO? What if it could report a diagnosis? …This is what “matchers” do.
A “matcher” is a predicate. We configure the matcher when we create it. It can evaluate any object. And when the object doesn’t satisfy it, the matcher can report detailed information.
Hamcrest is a matcher framework with a unique DSL: matchers are designed to be composed from other matchers. This provides unparalleled control. Matchers can easily express only what they care about. This makes tests less fragile.
Hamcrest is not only a library of matchers; it’s also a framework for writing your own matchers. So can we write a matcher to test that a URL contains a query item? See How to Make Your Own OCHamcrest Matcher for Powerful Asserts.
Have you extracted an assertion test helper recently? Which approach did you use? Click here to leave a comment.[This post is part of the series TDD Sample App: The Complete Collection …So Far]
Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He’s been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.