Write snapshot tests!
Today’s topic is not connected with RxSwift
at all. Recently, I’ve started using an awesome library so I decided to write an article about it, because it’s worth to mention. Don’t worry, I will come back with reactive stuff soon 😉
I hope you know how important unit tests are in writing software applications. In case you don’t know I recommend you my article series and Jon’s Raid website about unit tests in the iOS world.
However, it is difficult (or even impossible) to cover all the code you have with tests. Writing unit tests for classes inherited from UIView
was a hard task for me, because the outcome was not worth the time spent on it. As a result, I stopped writing unit tests for any of views.
In my last project, a colleague of mine introduced me FBSnapshotTestCase library developed by Facebook.
A job of FBSnapshotTestCase is to create a screenshot of the view and to compare it with the corresponding, reference image.
Snapshot tests for the business
You may ask, what are the benefits of using snapshot tests.
Comparing the output image of the view gives you a lot of feedback. It won’t let you or your team make some changes inside .xib
files by a mistake.
Every time when you want to change how the view looks like, you will have to regenerate reference image. Any changes won’t pass unseen through the code review.
A software which doesn’t change unexpectedly is a good quality product. Good quality product equals more clients, which ends with more money.
Moreover, snapshot tests are fast. In average one snapshot test takes few milliseconds to run. In case you don’t have such tests, the bug will probably come back after QA analysis. However, creating a JIRA ticket, writing a description, adding an attachment takes time! For sure more then few milliseconds 😉
Snapshot tests – a tool for a developer
Snapshot tests are very handy while building the view. You think you’ve finished building your .xib
. You run it on the device and yes… it looks good:
However, did you check if content hugging/compression resistance priorities are set up correctly? With a FBSnapshotTestCase it is a matter of adding an additional test case with long titles and description:
What with 4" and 3,5" screen size devices? Just create a separate snapshot:
Configuration
At the beginning install FBSnapshotTestCase. After adding pod 'FBSnapshotTestCase'
into Podfile
, you have to configure the path where reference images will be stored. The author of the library suggests you insert the path into scheme
inside the Xcode
. Unfortunately, it will work only inside Xcode
. If you use AppCode you will have to set the variable inside the AppCode as well. This is the reason why I prefer to set it via Preprocessor Macros
.
To set the path as a preprocessor macro do the following:
1. Open Xcode
2. Open project configuration file
3. Select test target
4. Open Build Settings
tab
5. Find Preprocessor Macros
6. Add FB_REFERENCE_IMAGE_DIR=@\"$(SOURCE_ROOT)/$(PROJECT_NAME)Test/ReferenceImages\"
Swift doesn’t allow you to read the value of any macros. In Swift, you can only check if a macro exists. To export the macro into an accessible variable you have to create string inside Objective-C Bridging Header. Inside Bridging Header insert:
static const NSString * _Nonnull snapshotReferenceImageDirectory = FB_REFERENCE_IMAGE_DIR;
The final step is to override FBSnapshotTestCase
and use exported variable:
class SnapshotTestCase: FBSnapshotTestCase {
override func getReferenceImageDirectory(withDefault dir: String!) -> String! {
return snapshotReferenceImageDirectory
}
}
Create your first snapshot test
Sooner or later you will have to create a subclass of UITableCell
. Let’s create snapshot test for it:
final class UserCellSnapshotTest: SnapshotTestCase {
var userCell: UserCell!
func prepareCell(with size: CGSize) {
let cellNib = UINib(nibName: "UserCell", bundle: nil)
guard let userCell = cellNib.instantiate(withOwner: nil, options: nil).first as? UserCell
else { fatalError("No nib named: \(nibName)") }
self.userCell = userCell
userCell.frame = CGRect(origin: .zero, size: size)
}
override func setUp() {
super.setUp()
//If you set recordMode as true, than the next test run will generate reference images. Remember to set it back to false, before pushing the code into repo
//recordMode = true
}
// The reference image will have the same name as the name of the test function
func test_userCellIsCorrect() {
prepareCell(with: CGSize(width: 375, height: 100))
userCell.title = "John"
// Those 2 lines beneath do all the magic
FBSnapshotVerifyView(userCell)
FBSnapshotVerifyLayer(userCell.layer)
}
}
A snapshot tests fails – What to do?
When a snapshots fails and you didn’t expect that to happen, thank yourself that you have snapshot tests. You’ve just avoided an unexpected butterfly effect 😉
Next, you have to diff the images to know what have changed. I use Kaleidoscope as a diff tool for images.
When a snapshot test fails FBSnapshotTestCase prints the command you need to run to open the diff inside Kaleidoscope. It is useful if you have just one failing test. If you have more of them I recommend you to:
1. Turn the recordMode
to true
again
2. Rerender the whole test case images
3. Run git difftool
command.
git difftool
command will open a Kaleidoscope app with all changes in compare to the last commit. I found this solution really helpful.
Kaleidoscope – the difftool
Kaleidoscope is a really great piece of software ;). First of all, it allows you to diff images, which is not possible with default FileMerge
.
What is more, it allows you to diff images in 4 different setups. I like one-up
mode mostly because you can set the automatic switch between images.
Sometimes snapshot tests may not work
Unfortunately, it is hard to use snapshot tests when the view is animated or when the view needs more time to render. Examples of such views could be MKMapView
and WKWebView
/UIWebView
. Sometimes adding some time interval may help, but not always.
The summary
First of all, I want to express my special thanks to @ochococo who showed me FBSnapshotTestCase – FBSnapshotTestCase is great, thank you! 🙂
Secondly, I hope you will start using snapshot tests. It is an another tool which decreases a chance of making unexpected changes inside the code which is huge for business. Unexpected changes can lead into losing a client which lowers the profit.
Snapshot tests are also helpful during the development stage. It is much easier to prepare snapshots for every devices and cases than to run each configuration on a simulator.
If you need more knowledge/arguments about snapshot tests – read the article on objc.io.
I hope you enjoyed reading 😉 – Stay tuned!