Test your icon font without snapshots
This is a follow up on: Where are your icons?, using an icon font on iOS
Hi there! Since you are already here, let's consider you have an iOS project using an icon font, which provides you with an easy library for all your icon needs. This is a very plausible approach to handle a large number of icons and create a standard source to work across multiple platforms.
But working with an icon font on a mobile project can bring some troubles along the way, and you may need some tips and tricks. So, what will we be doing here? Improving our unit tests for our icon's library.
Snapshot tests, the most obvious approach
You may be wondering, icons are images, therefore we can snapshot them and keep track of any new icons by simply comparing snapshots, right? Well, yes we can. But issues start to appear when we keep updating our icon font with new icons and maybe reaching hundreds of icons inside of it.
Using snapshots is a fairly high-cost approach to test, even though it certainly works. But if you are working with snapshots you may have two options:
- Making an individual snapshot for every single icon, which will make you end up with hundreds of snapshots recorded in your project.
- Making a single snapshot for your whole lib, and with a lot of icons, you'll probably end with a very large reference file for all the icons at once.
This second approach is what we discussed in this past article and also what we used to do here. But having a single snapshot means that the whole reference image would have to be updated along with any new icon addition, and we'd lose the actual testing property of it, which is comparing an icon from the new font with the previous one.
Let's think a little bit more about our tests, what do we want from them?
What do we want during a font update?
Every time we have a new version of our icon font, we'll update it on the project, parse every new icon and update our snapshots. But, like any other dependency update, something wrong may happen, for instance, some old icons could have changed or be broken. So, what can we do to make sure it worked and feel comfortable with our update? Testing.
- Test if the pre-existent icons have not been changed
- Test if the new icons work as expected
The second item is the easiest one when adding a new icon or updating the font, the developer may check if the icon is being rendered correctly by running the app. The first one is a little bit more tricky.
Since we don't want to store a bunch of reference images, what if we could compare our icons, without actually storing a snapshot for them?
A new test approach
Let's work with an idea: Create an object which allows us to compare the images without the need to actually store the images. Wait… what?
If we had the
UIImage for every icon, we could compare these images, but that's exactly what a snapshot test is and what we don't want to do. But, what if we could convert these images into a smaller, yet still comparable, object?
UIImage has some interesting properties, and one of them is converting the image into a
Data object by calling
.jpegData(), and the
Data object, represents the image and can be converted into a String.
And why are strings even more interesting? Well, Strings are very manipulable and usually much lighter and easier to store than images. But, just converting a
UIImage to data using
.pngData() and then into a String will not give us this so expected reduction in size. Mainly because we could later use this information to restore the image so they must contain roughly the same amount of data.
But the good thing is: We don’t need to restore the image from the data, we just need it to compare with other images! So we can manipulate the string, even with some non-reversible algorithm, reduce its size, and store the smaller String to compare.
Let's call this tiny String a hash and take a step back. We have now a nice idea for the new test we are creating, lets enumerate what we need to do:
- Create an image from the icon
- Convert it into a
- Convert the
Datainto a String hash
- Use the Hash to compare images
Creating an Image
There are many ways to create a snapshot for a
UIView, you can even use a snapshot library to do so, but let's try a more independent way to create a snapshot using the
First, create a view with a label, an
IconView, that displays a single icon from your icon font. Then, we can snapshot it using the image renderer. But there's a catch, the snapshot will only work on the test targets if your view is embedded into a
UIWindow. Make sure to remember this when snapshotting your view.
Create the Hash
We already have a snapshot for our icon, and now we need to convert it into our hash.
There are plenty of ways to do so, but we decided to go here with SHA256 encryption, which is a one-way encryption algorithm that provides a consistent String output given the same input, and the best part is: The output hash is much smaller than the entire image data.
You can use the SHA256 algorithm from the Apple’s CryptoKit. The next example is using a hasher we already had on the project.
Now, we just need to record our hashes and use them to compare with any new updates!
Thank the Swift gods for already making the String an
Before we can make our assertions and actually test our image hashes, we need to record the hash for each icon, and just like a snapshot test, we'll create a record mode that creates the hash for all the icons and store them.
To store the hashes, we decided to create a single JSON file with an entry for each icon. This JSON file is our storage and will provide the information to later compare the images. Create a
Codable struct with the information we are going to store on the JSON for each icon:
And now, with all the icons hashes we can store them!
To write a file locally, on the same or a close folder of your test file, we can get the URL of the Test file and adjust it to the path we need.
And now we can record our reference:
This should write all the information we need beautifully on a local JSON file.
When testing, we make almost the same steps as when recording, but, instead of storing the information, we use the hash to compare it with the previous one.
If the hash is the same as before, the icon had no changes. And after all that, we can finally create our test case:
You now have a new test that ensures that whenever the font is updated, the previous icons have not been changed, and if they do, the tests will fail just on the icons where the hash is different.
Here you've learned a little bit more about testing images for changes, without really comparing images. It's been really useful to keep our icon lib safe during the font file updates, it runs relatively fast for each icon, and its a lightweight test because it stores a single JSON file.
At first, we didn't know it was going to be a good solution or not, but given how our icon font is updated, where the existent icons are usually not modified and just new icons are inserted into the font, this test has been enough to make sure nothing has broken so far.
The downside of testing by comparing the hashes is that the test will not give you exactly what has changed (like a regular snapshot test by image comparison would) just that something is different and you'll need to check both font versions (or your design team) to see if this update was intentional.
Nonetheless, it's been a nice exercise to work a bit more with writing files, testing, encrypting, and creating snapshots.
I believe this example is much more interesting when thought about how multiple things can be brought together to be part of a solution. Keep learning new stuff and you'll always find clever ways to combine them to solve your problems.