Unit test SwiftUI views without a view model
Introduction
The most common architecture pattern in SwiftUI is MVVM. This is a great pattern, but it can be a bit overkill for simple views. In this post, we’re going to talk about how to unit test SwiftUI views without a view model. This is a great way to keep your code clean and simple, and it’s also a great way to write unit tests for your views.
Reasoning
Not every view needs a view model. I know this might be a controversial statement, but hear me out. If your view is simple enough, you can just use the view itself as the model. This is especially true for views that are just displaying data and don’t have any complex logic. In these cases, you can just use the view itself as the model and write your tests against it.
So if you’ve been writing a view with small logic, but you still want to make sure that your logic is fully covered, repeatable, and predictable, this is my way of doing it.
Static methods
This is the most obvious way to do it. You can just write a static method in your view and call it from your tests. This is a great way to keep your code clean and simple, and it’s also a great way to write unit tests for your views. It also helps with forcing you to have pure functions, which is something I personally love.
Here’s an example of how to do this:
The View
struct ContentView: View {
@State private var someString = "Hello, world!"
var body: some View {
VStack {
Text(someString)
Button("reverse string") {
someString = Self.reverseString(someString)
}
}
.padding()
}
static func reverseString(_ string: String) -> String {
String(string.reversed())
}
}
As you can see in this very important method that’s clearly carrying the business logic of the app, we have a static method that reverses a string. This is a pure function, and it doesn’t depend on any state. This is a great way to write unit tests for your views, and it’s also a great way to keep your code clean and simple.
The Test
import Testing
@testable import SwiftUI_unit_testing
struct ContentViewTests {
@Test
func testStringBeingReversed() {
let baseString = "abc"
let expectedString = "cba"
let reversedString = ContentView.reverseString(baseString)
#expect(reversedString == expectedString)
}
}
Simple enough, isn’t it? All we need to do is call the static method and check if the result is what we expect. This is a great way to write unit tests for your views, and it’s also a great way to keep your code clean and simple. There isn’t even a need to instantiate the view!
Pitfalls
Like everything else in life, there are upsides and downsides to this approach. Here are a few things to keep in mind:
- Pure functions might not be the right tool: I’m a fan of using whatever tool is right for the job. Sometimes, a pure function is just not the correct tool.
- State observability: If your view is using
@State
or@Binding
, you might need to use a different approach. The updates don’t show in the tests, so you could look into using UI tests to cover this. - Lack of context: SwiftUI views are just that - views. They’re visual components, and while we can test the logic, we can’t test the visual aspect of it. For that, you might want to look into snapshot testing or UI testing.
Conclusion
In this short post, we’ve discussed how to unit test SwiftUI views without a view model. As you can see, like anything else, it’s a decision and you should be mindful when you make it. Sometimes, a view model is just not needed. Other times, it absolutely is, if you want to test side effects of methods, different states, and so on. This should just be yet another tool in your belt. Don’t fall into the tribalism of “MVVM is the only way” or “MV is the only way.” Use the right tool for the job, and you’ll be fine.