SwiftUI Testing -- Part the First

Apple released SwiftUI (checks notes) in 2019. Wait what? It’s been 5 years?! Wow. In the beginning, the framework was a bit… sparse, but Apple has refined and improved it so now SwiftUI is worth considering for app development. But an important part of any framework we want to include in our apps is testability. How testable is SwiftUI? What are some of the hurdles we need to overcome in order to quickly and accurately test our SwiftUI interface and make testing an integral part of our development workflow?

SwiftUI aims to replace UIKit on iOS, so that’s the benchmark in terms of testing. Ideally, SwiftUI should be at least as good as UIKit in this respect. The key to testing with UIKit is getting a view controller to execute its lifecycle. In a blog post lost to the inevitable bit rot of the Internet, NatashaTheRobot showed that if you instantiate a view controller and access it’s view attribute, the system’s runtime would cause the whole lifecycle to execute. Using a Storyboard, the code for that looks something like this:

1let storyBoard = UIStoryboard(name: "MyStoryboard", bundle: nil)
2let viewController = sBoard.instantiateInitialViewController() as? MyViewController
3XCTAssertNotNil(viewController.view)
4

By accessing view, we trigger a whole slew of Apple magic to bootstrap the viewController’s lifecycle including things like viewDidLoad(), viewDidAppear() and deserializing any components from the storyboard. After that, we’re free to poke and prod the VC and thoroughly unit test everything. UIKit has really set the bar pretty high in terms of testability. But trail blazers in the dev community have had many years to work all this out. After five years of SwiftUI, how does it stack up?

Well, that’s where it gets tricky. With UIKit, we’re able to actually create the view controller in a test and interact with it directly. In SwiftUI, our View structs aren’t the actual interface, but a description of the interface. The SwiftUI framework takes that description and creates real interfaces with UIKit on iOS or AppKit on the Mac. So how do we test that? We need help. Ideally, Apple would provide that, but they don’t, so we need to look elsewhere. After five years, ViewInspector has become the de facto standard for filling that role. Honestly, I’m not sure the voodoo that’s involved with ViewInspector, but tl;dr, it lets us create a view, inspect the view hierarchy and interact with controls. Given that this is a third party, open source, community effort, ViewInspector is fantastic! While they cover most of the SwiftUI API, you can see on the readiness list, that some corners of the framework aren’t supported. And because our View structs are just descriptions of the UI and not the actual UI, I’ve found it quite fragile to work with anything but the most basic code in the non-UI parts of the View struct. But a View can often need to be quite complicated, so how do we deal with that? One word – plastics! – I mean ViewModel! Or is that two words?

Using a ViewModel, we talk all the logic (and I mean all the logic) out of the View and put it into a class we’ll call ViewModel. Even a simple if statement in the View moves to a computed property of the ViewModel. We’ll store an instance of ViewModel on the View and mark it as a @State variable. Testability of this configuration turns out to be quite high. In testing the View itself, we can easily build a fake subclass of the ViewModel and check that the view responds correctly to different states in the view model and calls the right methods with we interact with it via ViewInspector. The ViewModel on the other hand, is just a class with no SwiftUI magic at all, so we can test that in the traditional ways with fakes, mocks, etc.

Problem solved! Almost. While this works great in theory, in practice there are some potholes. Most of the problems we encounter are related to incomplete coverage of the SwiftUI API by ViewInspector. Again, the folks working on ViewInspector have done an enormous service to the community with all their hard work. But they are chasing a moving target, with new API released by Apple every year, some of which is difficult or even impossible for a third party tool to work with. But in the end, while this solution is not 100%, it’s robust enough to provide a comfortably high degree of testing. We’ve been using it successfully with GlowWorm for a while now.

Follow us on Mastodon and LinkedIn for more info Swift, SwiftUI and software testability. And if you’re interested in help with testing your existing app or building a whole new app, don’t hesitate to get in touch. We’d love to hear from you.