How do we automate tests for SwiftUI? Should we even bother? And does test-driven development (TDD) apply to SwiftUI?
SwiftUI Testing: The Challenge
In my TDD for iOS Workshop, participants start TDD by coding something without UI, networking, or persistence. I then introduce testing through the UI. The key idea is to use fast unit tests for UI-driven code instead of slow UI tests. People are often blown away at how easy and effective it is.

Improve your test writing “Flow.”
Sign up to get my test-oriented code snippets.
I’ve taught this many times using UIKit. What would it look like for SwiftUI? Well, what if I take the same exercise I’ve used and port it to SwiftUI? So here we go!
The challenge is to build a Counter app. “What’s that?” you may ask. Why, it’s a useful app for counting things.
Counter Demo
Here’s a demo of what it can look like. (This is the prettiest version I’ve ever made, thanks to suggestions from the Claude LLM.)
As you can see, the idea is simple. There’s a count and two buttons. One button increments. The other button lets you clear the count, but uses an alert to confirm this. After all, it would be a bummer to reset the count by mistake.
Counter Source Code
Here’s one way to code the main guts of the Counter:
struct CounterView: View {
@State private var count = 0
@State private var showingClearAlert = false
var body: some View {
VStack(spacing: 24) {
Text("\(count)")
.font(.system(size: 96, weight: .bold, design: .rounded))
.foregroundStyle(.primary)
.contentTransition(.numericText(countsDown: count == 0))
.animation(.spring(), value: count)
HStack(spacing: 16) {
Button(action: { count += 1 }) {
Label("1", systemImage: "plus.circle.fill")
.font(.title2.weight(.semibold))
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.borderedProminent)
.tint(.green)
Button(action: { showingClearAlert = count > 0 }) {
Label("Clear", systemImage: "trash")
.font(.title2.weight(.semibold))
.frame(maxWidth: .infinity)
.padding()
}
.buttonStyle(.bordered)
.tint(.red)
.opacity(count > 0 ? 1 : 0.5)
}
}
.padding()
.alert("Clear Counter?", isPresented: $showingClearAlert) {
Button("Cancel", role: .cancel) { }
Button("Clear", role: .destructive) {
count = 0
}
} message: {
Text("This will reset the counter to zero.")
}
}
}Take a look through this code. Then ask yourself…
How Would You Test This?
Before I show you my approach, I’d love to hear your thoughts:
- Would you unit test this? Why or why not?
- If you were to unit test this, how would you do it?
- What parts are worth testing, and what parts aren’t?
- If you were to build this using TDD, how would you approach it? What would your test list look like?
Share your thoughts in the comments below!
This post kicks off our “Let’s TDD a SwiftUI App” series. In the next post, we’ll compare unit testing with UI testing, and explore why unit testing SwiftUI is worth the effort.
Articles in this series
Let’s TDD a SwiftUI Counter App
Don’t miss a single post in this exciting series.
Subscribe to my newsletter, and I’ll notify you whenever I wrote a new blog post. As a bonus, you’ll also receive the test-oriented code snippets I like to use in Xcode.


I’ll take a shot!
> Would you unit test this?
> What parts are worth testing, and what parts aren’t?
I’ll answer these two at the same time since they go together in my mind. When I look to unit test something I ask myself, “Is there behavior to define?” In this case, there is behavior depending on the value of `count` and `showingClearAlert`. For example, I might test “when I increment the count, it always only goes up by one” and “clearing the count means setting it to 0.” But I might avoid testing “The label text is the count.”
> If you were to unit test this, how would you do it?
The first challenge I see is that since `CounterView` is a View, we’re not able to unit test it directly (out of the box, at least). I would try to use safe refactorings to create a view model or `Counter` type that could be tested in isolation of the UI. I would also extract the SwiftUI button actions to methods on the view model so that this behavior could be tested.
> If you were to build this using TDD, how would you approach it? What would your test list look like?
– View model starts with a count of 0 and without the clear alert visible.
– The counter increments by 1 (cases: 0 -> 1, 1 -> 2, 100 -> 101)
– Clearing turns the count to 0 (cases: 0 -> 0, 1 -> 0, 100 -> 0)
– Requesting clear sets the `showingClearAlert` flag if `count` is greater than 0.
(After writing this test case I notice that there is an action to actually “clear” and also something like “request clear prompt if necessary”. Some clever naming might be in order here!)
Looking forward to seeing your solution, Jon!
Hey Ricky! Thanks for jumping in to “warm the engines”
I agree with Ricky.
Adding to his thoughts, I think another test that verifies we‘re actually calling the right methods from the view (and perhaps displays the right values) could improve our test suite. I know there are UI tests and the ViewInspector library for those.
Bilal, I’ll have much more to say about ViewInspector. And thanks for mentioning UI tests, I should touch on how I avoid them.
Let me add a few ideas, so you can show how painful it is to test the UI directly ;)
(*) The “+1” button should always be enabled
(*) Pressing the “+1” button should increment the count
(*) The “clear” button should be disabled when the counter == 0
(*) The “clear” button should be enabled when the counter > 0
(*) Pressing the “clear button” should lead to the alert with “cancel” and “clear” options.
(*) Pressing the “clear” option on the alert should reset the counter to 0 (and disable the “clear” button)
(*) Pressing the “cancel” button on the alert should keep the counter unchanged and both buttons enabled.
I assumed we would not be testing colo(u)rs, font sizes, system images, and the displayed text. Is that an incorrect assumption?
Nice test list, Joe. “How painful it is…” well, we’ll see!
Your last list of “I assumed we would not be testing” is a great point to discuss.
Hey! I hope I’m not too late to the party.
I just wanted to share some thoughts based on my own experience.
> Would you unit test this? Why or why not?
Yes! Definitely! There are two approaches here: one for those who want to use ViewInspector and another for those who don’t. Initially, I was full of hope when I saw that ViewInspector had an API that could locate a button declaration in a View definition and trigger its closure. However, recently, this has caused many issues like tests flakiness or tests that fail to compile, especially when switching from Xcode 15 to Xcode 16. This experience changed my approach to unit testing in SwiftUI.
> If you were to unit test this, how would you do it?
In current approach I start with snapshot test and preview definition (both can share the same code). Then I implement the pure view leaving all actions empty and a single struct that holds the whole dynamic content of a screen. When the UI is ready I focus on behaviours.
With ViewInspector (my old way)
I write a test in a way that first I perform an action e.g I “tap” on a button with a given name and then I expect my state struct to change accordingly.
Without ViewInspector (new way)
I start with writing tests for behaviours that can happen on the screen like “plus button is tapped”. I test my reducer by scheduling each action and expecting a new correct state.
Shortly
* snapshot tests covers UI part
* unit tests covers business logic (in my case stored in reducer)
* the untested part is the connection between UI element action and component that schedules actions (state store in my case)
* there might be a problem with snapshotting part of modifiers (e.g. alert view modifier)
> What parts are worth testing, and what parts aren’t?
Everything is worth testing. I’ve often heard arguments that simple things aren’t worth testing, but I disagree. I’ve made simple mistakes more times than I can count, and having tests in place has saved me many times.
Unfortunately, without ViewInspector, it’s not possible to test everything using my approach. The part that remains untested is the connection between UI element actions and the component that schedules actions (in my case, state stores). There’s a space for mistake here, but I have to accept that risk, because of ViewInspector imperfections.
> If you were to build this using TDD, how would you approach it? What would your test list look like?
1. snapshot test where counter = 0
2. snapshot test where counter > 0
3. unit test for screen state after the plus button is tapped
4. unit test for screen state after the clear button is tapped
5. unit test for screen state after the clear pop-up button is tapped
Thanks for sharing, Maciej! You point out an inherent danger of 3rd-party testing tools. I’ll be sure to mention that when I write about ViewInspector, thank you. I also experienced trouble going to Xcode 16, but find that the maintainer Alexey is quick to provide help. This was due to a change in how Apple creates views without our knowledge.
You do a nice job of describing the non-ViewInspector approach.