11

I am trying to write unit tests for SwiftUI views but finding zero resources on the web for how to go about that.

I have a view like the following

struct Page: View {
@EnvironmentObject var service: Service

var body: some View {
    NavigationView {
        ScrollView(.vertical) {
            VStack {
                Text("Some text"))
                    .font(.body)
                    .navigationBarTitle(Text("Title")))

                Spacer(minLength: 100)
            }
        }
   }
}
}

I started writing a test like this

func testPage() {
    let page = Page().environmentObject(Service())
    let body = page.body
    XCTAssertNotNil(body, "Did not find body")
}

But then how do I get the views inside the body? How do I test their properties? Any help is appreciated.

Update: As a matter of fact even this doesn't work. I am getting the following runtime exception

Thread 1: Fatal error: body() should not be called on  ModifiedContent<Page,_EnvironmentKeyWritingModifier<Optional<Service>>>.
user1366265
  • 1,306
  • 1
  • 17
  • 28
  • 3
    I really don't think SwiftUI was designed to be unit tested like that. I would go with UI testing for testing the UI. – rraphael Oct 04 '19 at 15:06
  • 1
    When testing UI, e2e/integrations are almost always preferred over unit tests. When you try to write unit tests, you actually end up testing the UI library itself or you end up duplicating the actual code in tests. I something is testable, it can be usually removed from UI to the business layer and tested separately. Swift UI defines layout. Trying to unit test Swift UI is like trying to unit test a xib or a CSS file. The best uni test you can do is a snapshot test. – Sulthan Oct 04 '19 at 20:18
  • 2
    Snapshots are great for layout, but SwiftUI is layout + behavior. – Jon Reid Oct 11 '19 at 17:50

2 Answers2

11

There is a framework created specifically for the purpose of runtime inspection and unit testing of SwiftUI views: ViewInspector

You can extract your custom views to verify the inner state, trigger UI-input side effects, read the formatted text values, assure the right text styling is applied, and much more:

// Side effect from tapping on a button
try sut.inspect().find(button: "Close").tap()
let viewModel = try sut.inspect().view(CustomView.self).actualView().viewModel
XCTAssertFalse(viewModel.isDialogPresented)

// Testing localization + formatting
let sut = Text("Completed by \(72.51, specifier: "%.1f")%")
let string = try sut.inspect().text().string(locale: Locale(identifier: "es"))
XCTAssertEqual(string, "Completado por 72,5%")

The test for your view could look like this:

func testPage() throws {
   let page = Page().environmentObject(Service())
   let string = try page.inspect().navigationView().scrollView().vStack().text(0).string()
   XCTAssertEqual(string, "Some text")
}
nalexn
  • 10,615
  • 6
  • 44
  • 48
  • Wgat purpose does this test have? Checking that a text in a view matches a string seems useless and sounds like a unit test would be bad practice, rather use a UI test here. As you are... testing the UI. Save unit tests for your business logic/controllers? – Jerland2 Jan 09 '21 at 12:52
  • @Jerland2 this test has the purpose of showing that SwiftUI isn't a black box. There are plenty of scenarios when SwiftUI view can alter its content based on the state changed by the business logic, and sometimes checking for the Text's string makes perfect sense. – nalexn Jan 10 '21 at 10:32
  • 7
    This is not the correct answer. @nalexn I have seen frameworks like this come and go for many years, they never last. Then the people who adopted them ended up building their software incorrectly. You should not be checking any logic within your UI. And vice versa. You should have an architecture that supports separating your logic out from your UI. Then you unit test it. The best part about unit testing is that it should be very fast. If people have to download your framework, then run UI tests to test the app then that will greatly slow down development. – Jon_the_developer Mar 02 '21 at 13:39
  • 1
    @iOSDev9381 this library does not encourage writing business logic in the UI. You can verify the strings are formatted and localized correctly, none of the labels are missing styling, and many other UI-related "logic" that is impossible to unit test otherwise, no matter how you architect your app. You assumption about the speed is wrong - the whole test suit for the library (it's 800 UI tests) is running under 1.5 seconds, it's a completely different experience compared to UI testing in UIKit. – nalexn Mar 03 '21 at 08:39
7

Update: Let's all try using the ViewInspector library by nalexn!

Original reply:

Until Apple

a) designs testability into SwiftUI, and

b) exposes this testability to us,

we're screwed, and will have to use UI Testing in place of unit testing… in a complete inversion of the Testing Pyramid.

Jon Reid
  • 20,545
  • 2
  • 64
  • 95
  • why can't we use snapshot testing for that particular view no need launch complete application like incase of UITesting – SreekanthI Sep 12 '22 at 19:48
  • @SreekanthI I like your idea. Keep in mind that while snapshot tests are much faster and more stable than UI tests, they are slower and less stable than microtests. But while I want to avoid using snapshot tests to test logic (as opposed to appearance only), I think snapshots could prove to be a useful middle ground for SwiftUI. – Jon Reid Sep 12 '22 at 23:31