← Home Archive SonicBunny Mastodon GitHub LinkedIn
  • SwiftUI Testing -- Part the Second

    by Jay Lyerly

    We’re continuing our look at unit testing with SwiftUI. In Part One, we looked back at how we tested UIKit and what approach makes sense to validate our SwiftUI projects.

    For unit testing, test coverage is an important metric. How much of your code base are you exercising during unit tests? Xcode is, unfortunately, not too bright when it comes to measuring coverage. It doesn’t have the intelligence to know if you’re “testing” a line of code, just that the line of code was executed. This is a problem right off the bat.

    I’ve got a new app project in Xcode. The app doesn’t do much yet and I want to start adding tests before it gets too big. So I add a unit test target and Xcode plops in some empty tests. I run the unit tests, and they all pass since they’re empty. I check the coverage and it’s at 36%! How can that be, I didn’t test anything?

    Xcode, the blissful idiot, is really reporting that 36% of the code was executed while running the unit tests. Usually, when an app starts up, it does some bootstrapping. You might set up your persistent storage, draw a couple of Views on screen, and maybe talk to the network. Xcode counts all that as “testing” because it ran during a unit test.

    To make the test coverage more accurate, we need to do as little as possible outside of our actual unit tests. Jon Reid has a write-up of how to do this with a UIKit app by swapping out the app delegate during tests. But SwiftUI introduces a whole new startup sequence so we need a new approach for SwiftUI’s new app lifecycle.

    After some futzing around, turns out it’s easy!

     1struct TestApp: App {					// 1
     2    var body: some Scene {
     3        WindowGroup {
     4            Text("I'm running tests!")
     5        }
     6    }
     7}
     8
     9@main										// 2
    10struct TestDriver {
    11    static func main() {
    12        if NSClassFromString("XCTestCase") != nil {    // 3
    13            TestApp.main()
    14        } else {
    15            MyRealApp.main()
    16        }
    17    }
    18}
    

    There are three key points that make this work:

    1. We need a dummy `App` struct to use instead of the real app. This simple stand-in circumvents all your usual app startup machinery. Instead of all the normal bootstrapping, we'll just get a window with the text "I'm running tests!".
    2. Remove the `@main` from your `App` implementation and add it here to `TestDriver`. Swift uses `@main` to figure out how to start your app. The `App` protocol provides a default implementation that, according to the docs, 'manages the launch process in a platform-appropriate way'. But by inserting our own wrapper layer here around, we can control _which_ `main()` is called.
    3. That brings us to the final point, use the good old `NSClassFromString` to decide if the testing bundle has been injected into our process. `XCTestCase` is only available during testing, so this is a reliable way to decide if unit testing is underway. Based on that, we can call the `main` method of either our real app or our testing stand-in. It turns out that the default implementation of `main` knows to use its parent struct to bootstrap the SwiftUI app.

    Now when I run unit tests, my coverage is at 0.8%! That’s more like it. In order to boost my test coverage, I now have to actually test code. And the code coverage metric really starts to mean something.

    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.

    → 4:24 PM, May 1
  • SwiftUI Testing -- Part the First

    by Jay Lyerly

    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.

    → 3:46 PM, May 1
  • RSS
  • JSON Feed