Swift Regex Deep Dive
iOS MacOur introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
Apple’s bundled test framework XCTest provides a very limited, general collection of assertions. These get the job done much of the time, but sometimes they’re not the right tool to communicate what you’re actually checking. That’s when it’s useful to know how to write custom assertions that clearly express what you are truly checking for without getting lost in a maze of little this-that-and-that assertions.
There are some signs it’s time to introduce your own, custom assertion:
Let’s go over those in slightly more detail.
Basically, you “just” extract method.
In practice, you need to be aware of a few tricks if you want to provide the same experience you’d get with the baked-in assertions.
There’s just enough to worry about that it’s a pain, so I’ll give you a snippet you can add to Xcode to make this easier at the end.
Say you have a test like so:
class MessageTests: XCTestCase {
let sender: Messager = …
var message: Message!
override func setUp() { message = … }
func testResendingOnlyUpdatesSentTime() {
let resentMessage = message.resend()
XCTAssert(message.sent.earlierDate(resentMessage.sent) == message.sent,
"failed to make resent time later: was (message.sent), "
+ "resending made it (resentMessage.sent)")
XCTAssertEqual(message.id, resentMessage.id, "id")
XCTAssertEqual(message.text, resentMessage.text, "text")
XCTAssertEqual(message.sender.name, resentMessage.sender.name,
"sender name")
XCTAssertEqual(message.sender.host, resentMessage.sender.host,
"sender host")
}
}
Notice the oodles of assertions. This is distracting!
But also notice what they’re really doing: providing field-by-field granularity for equality checking.
Even if the Message
class were to implement Equatable
and we were simply checking all fields were equal, we still might want a helper assertion that checks each field separately to simplify debugging by providing more granular diagnostics.
This is especially valuable if you have a large bundle of data where figuring out what values are not equal by visual inspection can be difficult, because you’re looking for a needle in a haystack. It’s a lot more helpful for a failing test to report, “Hey, this one field was wrong,” than, “One of these fifty fields is wrong, and some of them are collections. Good luck!”
Let’s grab that sequence of assertions and yank them out into their own method.
For want of a more clever name, let’s call the fields that don’t change on resend “durable fields”, and call our assertion AssertDurableFieldsEqual
.
Behold! The power of extract method:
func testResendingOnlyUpdatesSentTime() {
let resentMessage = message.resend()
XCTAssert(message.sent.earlierDate(resentMessage.sent) == message.sent,
"failed to make resent time later: was (message.sent), "
+ "resending made it (resentMessage.sent)")
AssertDurableFieldsEqual(message, resentMessage)
}
}
func AssertDurableFieldsEqual(
message: Message, _ otherMessage: Message
) {
XCTAssertEqual(message.id, otherMessage.id, "id")
XCTAssertEqual(message.text, otherMessage.text, "text")
XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name")
XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host")
}
This has one problem: Any failing assertions in AssertDurableFieldsEqual
will log a failure against the line in that assertion method rather than in the test that’s using our assertion method. This makes it a lot harder to pinpoint what’s failing, especially if you have multiple tests using the custom assertion with multiple failures, because that leads to errors stacking up, and Xcode’s UI for that is not fun.
It turns out the file and line number blaming is done through default arguments. For example, have a look at the definition of XCTFail
, which you’d call like XCTFail("should never be executed!")
:
public func XCTFail(
message: String = default,
file: String = default,
line: UInt = default
)
Those defaults turn out to be the compiler-provided identifiers #file
and #line
. Let’s add those arguments and defaults to our custom assertion so they get captured in the test method calling our assertion, at the call site:
func AssertDurableFieldsEqual(
message: Message, _ otherMessage: Message,
file: StaticString = #file, line: UInt = #line
) {
XCTAssertEqual(message.id, otherMessage.id, "id")
XCTAssertEqual(message.text, otherMessage.text, "text")
XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name")
XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host")
}
Take care to note that line
’s type should be UInt
, not the more common Int
. You won’t run into any problems just yet, but you’ll get yelled at by the compiler if you use Int
instead of UInt
once you do the next, and final, step.
There’s still one more step to get proper blaming on failure: We also need our custom assert method to tell all the assert methods it uses what file and line number should take the blame for failure.
We do this by explicitly providing the file
and line
arguments
to all assertions rather than allowing them to default to the file and line in our custom assertion method itself:
func AssertDurableFieldsEqualPipeFileAndLine(
message: Message, _ otherMessage: Message,
file: StaticString = #file, line: UInt = #line
) {
XCTAssertEqual(message.id, otherMessage.id, "id",
file: file, line: line)
XCTAssertEqual(message.text, otherMessage.text, "text",
file: file, line: line)
XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name",
file: file, line: line)
XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host",
file: file, line: line)
}
And that’s it! It’s kind of obnoxiously boiler-plate-y, but we now have an assertion that acts just like any baked-in XCTest assertion.
I have this snippet in my Xcode to simplify a few of the less-obvious bits that I might forget. I bet you’ll find it useful as well:
func Assert<#Something#>(
<#arg#>,
file: StaticString = #file, line: UInt = #line
) {
XCTAssertTrue(true, file: file, line: line)
}
The “assert true” bit is there to remind me to pipe in the file and line. The rest is there so I don’t have to type out the file and line args and their defaults, but can get right to the part I care about.
I have it saved with completion shortcut jwsassert
. Using your initials as a prefix for your custom snippets gives you autocomplete lookup of all your snippets just by typing a few characters.
, file: file, line: line)
copy-pasting).Happy testing!
Our introductory guide to Swift Regex. Learn regular expressions in Swift including RegexBuilder examples and strongly-typed captures.
The Combine framework in Swift is a powerful declarative API for the asynchronous processing of values over time. It takes full advantage of Swift...
SwiftUI has changed a great many things about how developers create applications for iOS, and not just in the way we lay out our...