April 19, 2022

How to Verify Arrays and See Simple, Clear Diffs using ApprovalTests.Swift

Click to play

0  comments

I’m Jon Reid, one of the maintainers of ApprovalTests.Swift. Today, we’re going to talk about verifying arrays.

If you haven’t already done so, I suggest you watch the earlier episodes:

verify for Objects, verifyAll for Arrays

Let’s dive into arrays. Here, I’m starting a new test where I want to test the sorting of names. First, we need some names, so let’s use the names of the dwarves in The Hobbit. Next, let’s sort them into a new array.

let names = TheHobbit.dwarves.sorted()

And then let’s verify that this sorting is correct. So we do try Approvals. Now when you verify an object, you call verify and pass in the result.

try Approvals.verify(names)

But if we do this and run tests, you’ll see that we get the default description with all the items on a single line:

["Balin", "Bifur", etc.]

To make this easier to read, instead of calling verify, let’s call verifyAll.

func test_sortingOfNames() throws {
    let names = TheHobbit.dwarves.sorted()
    try Approvals.verifyAll(names)
}

Now we get one line per item, along with its array index:

[00] = Balin, [01] = Bifur, etc.

Adding a Label

We can add more meaning to this by adding a label. For this case, let’s try a label of “name” to see what we get.

try Approvals.verifyAll(names, label: "name")
name[00] = Balin, name[01] = Bifur, etc.

And now we have a list of all the names, and it’s very easy to read them and see that they’re in the right order. To the left of each index, you can see where it uses the label we gave. And if we like this, we can approve the results. Now when we run the tests again, they pass.

Received on left copied to Approved on right

Clearer Mismatches than XCTAssertEqual

I want to compare this against a normal XCTest assertion for equality. This is the same test, but using XCTAssertEqual. It can take some work to get the expected result correct. But once it’s written, it’s not hard to read.

func test_sortingOfNames_oldWay() throws {
    let names = TheHobbit.dwarves.sorted()
    XCTAssertEqual(names, [
        "Balin",
        "Bifur",
        "Bofur",
        "Bombur",
        "Dori",
        "Dwalin",
        "Fili",
        "Gloin",
        "Kili",
        "Nori",
        "Oin",
        "Ori",
        "Thorin",
    ])
}

Now watch what happens if I change the spelling of one name. If we change Fili to Filli with two L’s, and run the XCTest version, look at the error we get. First, in Xcode, we have to click the error to expand it. There’s the full error message:

XCTAssertEqual failed: ("["Balin", "Bifur", "Bofur", "Bombur", "Dori", "Dwalin", "Filli", "Gloin", "Kili", "Nori", "Oin", "Ori", "Thorin"]") is not equal to ("["Balin", "Bifur", "Bofur", "Bombur", "Dori", "Dwalin", "Fili", "Gloin", "Kili", "Nori", "Oin", "Ori", "Thorin"]")

Can you tell where the mismatch is?

I’ve spent a lot of time on test errors like these, trying to find the differences. I usually have to copy both sides into a diff tool. The latest version of Kaleidoscope does make this easier by letting you drag test results straight in.

But why not show the difference right in a diff tool, to begin with? Then the difference is highlighted for us with no extra steps. And if the change is good, we can approve it right there. That’s the power of using ApprovalTests.

Diff tool highlights Filli as different from Fili

I've spent a lot of time trying to find the differences in test errors. Why not show the difference right in a diff tool? And if the change is good, we can approve it right there. #ApprovalTests

Click to Tweet

Comparing Relative Order, Not the Indices

Now by default, verifyAll writes out the array indices. So those indices are part of the comparison. But what if you don’t care about the indices themselves? What if you only care about the relative order of items?

Let’s undo our misspelling and add a new name. If we run the test again, it shows how the new name changes the array indices of the names that follow. If we only care about their order, this is too much information.

name[10] doesn't match name[9], etc.

So let’s simplify the output.

First, let’s comment out the new name and get back to tests passing. Then instead of specifying a label, let’s provide a labeler. This is a closure that transforms each array item into a string. So let’s have this labeler output only the input, the zeroth argument, and nothing more.

try Approvals.verifyAll(names, labeler: { $0 })

Now if we run the tests, we get the names only, and no indices.

Balin, Bifur, on separate lines without indices

Let’s approve that, and rerun the test to see it pass. And now let’s add that new name back in. When we run the test, we can easily see what’s been added or removed. Then it’s up to us to decide if we want to keep the change or not.

Without indices, addition of new name pops out

Conclusion

So that’s how you can use ApprovalTests to verify arrays.

  • Use verifyAll and an optional label to get everything, including the array indices.
  • Use a labeler with a simple closure if you only care about the relative order, not the indices themselves.

In upcoming videos, I’ll show you more about the power of this labeler closure.

If you have any questions about ApprovalTests.Swift, please ask in the comments below. You can also send a tweet to me on Twitter, @qcoding. And add the hashtag #ApprovalTests. Thank you for watching.

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy.

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Never miss a good story!

Want to make sure you get notified when I release my next article or video? Then sign up here to subscribe to my newsletter. Plus, you’ll get access to the test-oriented code snippets I use every day!

>