Whilst working on ActionBuilderTool, I was getting an annoying test failure.

A couple of my tests generate a workflow, and compare it against the expected text, which is 50+ lines of text.

Unfortunately the basic #expect(generatedYAML == expectedYAML) implementation doesn’t do a very good job of explaining where the mismatch is…

When the #expect macro gets a false result, it is clever enough to dump the source code of the comparison expression you give it, and also the values of both sides of the comparison.

That’s pretty useful for small values, but for large blocks of text, it’s not so helpful.

As it happens, I’ve been here before, with the XCTest framework, and I wrote myself a bunch of utilities, including some better code for matching two strings, lists or dictionaries against each other, and identifying the first difference between them.

The XCTestExtensions package contains more than just the comparison code, and at some stage I realised that the item matching part of it might have wider applications, so I factored it out into its own package.

I didn’t know at the time that the Swift Testing framework was on the horizon, but it turns out that it was, and that having Matchable as a standalone package is very helpful as it allows it to be used directly with Swift Testing based tests.

The README I wrote for Matchable is actually pretty comprehensive, and for full details, I suggest you go there.

I’ve copied & pasted some highlights below.

Matchable

The matchable protocol defines a way to compare two objects, structures or values for equality.

Unlike the Equatable protocol, Matchable works by throwing an error when it encounters a mismatch.

You can view this as an assertion of equality. For this reason, the primary method is named assertMatches.

This makes for compact code since you don’t need to write explicit return statements for every failed comparison.

It also allows the protocol to handle compound structures intelligently.

If a matching check of a structure fails on one of its members, the matchable code will wrap up the error thrown by the member, and throw another error from the structure.

Any catching code can dig down into these compound errors to cleanly report exactly where the mismatch occurred.

Usage

You can check that two values match with:

try x.assertMatches(y)

A sequence of checks can easily be performed – the first failure will throw, causing the remaining checks to be skipped:

try int1.assertMatches(int2)
try double1.assertMatches(double2)
try string1.assertMatches(string2)

A type can implement matching by conforming to the Matchable protocol, and defining the assertMatches method. Inside this method it can perform the necessary checks.

If it finds a failure, it can throw a MatchFailedError to report the mismatch.

Implementations of assertMatches are provided for most of the primitive types, and a few Foundation types (I’ve just done the ones I needed for now - pull requests gratefully received…).

Compound Types

Although you can match primitive value types, the protocol comes into its own when performing memberwise matching of compound types (structs, objects, etc).

In this case a type can conform to the MatchableCompound protocol, and defining the assertContentMatches method.

This works the same way as the basic assertMatches method, except that if a check throws an error inside this method, the error will be wrapped in an outer error reporting that the whole structure failed to match.

See the README for more details on this.

Swift Testing

With Swift Testing, you can just marks your test method as throws, and then place a call to try thing1.assertMatches(thing2) into it.

The nice thing about Matchable is that the error it throws contains enough information to pinpoint where the match failed.

If the assertion fails, you’ll see a relatively well formatted report showing where it went wrong.

For example:

------------------------------------------------------------
Matching failed at line 149 of ActionBuilderCoreTests.swift.
------------------------------------------------------------

strings different at line 13.

was:

"- name: Make Logs Directory"

expected:

"- name: Select Xcode Version"

There’s Gold In Them Thar Hills

That’s enough rambling for this diary entry. I have been doing more on ActionBuilder itself, but that will have to wait for another day.

One last note though. Like quite a lot of the code I’ve written over the last few years, I just sort of got on with Matchable.

I made it open source, but never really got around to telling people about it.

Partly I just didn’t find the time to write a post like this. Partly perhaps I didn’t want to commit to fully supporting a whole load of open source software. Possibly imposter syndrome kicked in and I didn’t think it was good enough.

Whatever!

The point is, there’s quite a lot of stuff like Matchable knocking around at https://github.com/elegantchaos.

I invite you to take a look at it, and especially, to get in touch if there’s anything that looks useful. It may be a bit out of date, but it’s likely to be easily fixable and I will probably be keen to help to bring it back to life.

Similarly, I can’t help feeling that a lot of it has been overtaken by advances in Swift, or other third party work. I’m equally interested to hear about that too.