I was reading a post about testing in SwiftUI and the author said that don't write unit tests for your View Models that tests the UI of your view. The example he gave was a simple counter app.
class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func increment() {
count += 1
}
}
struct ContentView: View {
@StateObject private var vm = CounterViewModel()
var body: some View {
VStack {
Text("\(vm.count)")
.accessibilityIdentifier("countLabel")
Button("Increment") {
vm.increment()
}.accessibilityIdentifier("incrementButton")
}
}
}
And here is the unit test:
final class CounterTests: XCTestCase {
func test_user_can_increment_count() throws {
let vm = CounterViewModel()
vm.increment()
XCTAssertEqual(1, vm.count)
}
}
The author said that don't write such tests which are using VM to verify the UI. The reason he gave was that the test is testing the implementation details and not the behavior and it will pass even if there is no Text view on the view.
Instead he advised to use UITest to check the actual UI and write only tests for VM that contains any logic (meaning not verifying anything on the view). Does that make any sense? I have hundreds of VM tests that are verifying the user interface. Should I replace them with UITests?
-
1I am not an expert on ios and Swift, but can you explain why this is an example of a unit test "using the VM to verify the UI"? For me, it looks like the ContentView does not appear anywhere in the test, so this looks like to a test for the VM exclusively.Doc Brown– Doc Brown2023年02月14日 17:39:04 +00:00Commented Feb 14, 2023 at 17:39
-
1The classic advice is to not test the volatile and difficult to test UI but instead test the API you put behind it, which for you is the ViewModel. The more abstract pattern this follows is called: Humble Object. Are you sure you read the advice correctly? Where is this SwiftUI post?candied_orange– candied_orange2023年02月14日 17:56:13 +00:00Commented Feb 14, 2023 at 17:56
-
Here is that tweet. twitter.com/azamsharp/status/… From what I am understanding the author is not saying VM testing is bad, he is just saying that don't use unit tests for VM to satisfy UITesting, instead use actual UITest project.john doe– john doe2023年02月14日 18:32:26 +00:00Commented Feb 14, 2023 at 18:32
-
@DocBrown Thanks! I believe that is the point author was making that developers are writing unit tests against their VM and checking whether certain properties are set correctly or not and believing that their UI is going to work correctly.john doe– john doe2023年02月14日 18:36:35 +00:00Commented Feb 14, 2023 at 18:36
-
3@johndoe to me the tweet reads like someone promoting their UI testing tool. Again, the classic advice is to remove all interesting code from the UI so it doesn’t need testing. Test interesting code. Make interesting code testable. Avoid tools that add magic and claim to make everything testable.candied_orange– candied_orange2023年02月14日 19:54:49 +00:00Commented Feb 14, 2023 at 19:54
1 Answer 1
This sounds like a mixture of some valid points with some misconceptions. So let me try to untangle this knot:
A view model is typically a component which holds the core logic of a view. It can exist without the view (hence it is not "an implementation detail" of the view), and tests for it are usually unit tests. Unit tests, when done correctly, have some pros and cons when compared to other tests like integration tests. On the pro side, you find
- when they fail, the area for the failure's cause is small
- since the VM holds only the core logic of the UI, tests become independend from any visual aspect of the UI, which makes them quite stable (and not brittle, that's where the author of that tweet is 100% wrong)
- they are quick
- a view model may provide certain properties or members which support easier testing or validation, even when those properties are not displayed directly in some UI element.
On the con side, they don't test the related view and don't guarantee the UI - as it is observed by a user - behaves as expected. That's where the author is right.
Views in the UI require a view model. For creating automated tests on the view, it usually is not worth to mock the VM out, hence these tests will typically be integration tests (testing the view together with it's view model).
On the pro side is this: when a UI test succeeds, one can be a little bit more confident that the UI behaves as intended for the tested aspect - that is indeed a benefit over pure VM tests. On the con side, however, there is this:
such tests are typically slower than unit tests (this may be negligible, or not, depending on the involved UI elements and the framework)
when they fail, the cause might be either in the view itself or in the view model, hence it may be more effort to find the root cause
real UIs for a certain view contain often visual design stuff, which tends to change more often than the underlying view model. Still such changes can break the tests and produce false positives. (The example in the question is too contrived to show this.) Hence tests using the view are often more volatile than tests for a corresponding VM.
So if we are in a situation where we can test the same functionality through a view or by directly accessing the view model, should we prefer the former or the latter (or write tests for both)? I think you have to find this out by yourself, the answer may be different per case. Both kind of tests have their pros and cons and it may be most effective to use a mixture of both, UI tests where you expect the UI elements to change rarely, and VM tests where you think it gives you a better cost/benefit relationship when you add direct tests apart from the view.
Finally, your existing VM tests are definitely not worthless. They test an essential part of your application, so I would keep them, even when you decide to add some View tests which verify some overlapping functionality.
-
Thank you! I was able to get in touch with the author and he clarified his post and said that he was only talking about not using VM to test the actual view but having unit tests for VM is perfectly fine. The main argument he was giving was that the VM test will still pass even if you don't have a view so that is why don't use VM to test a View instead rely on the actual UITest (the slower one). He did emphasize that having tests for VM, where you are testing the actual logic is perfectly fine.john doe– john doe2023年02月14日 23:42:42 +00:00Commented Feb 14, 2023 at 23:42
-
I like your approach of writing few tests for the View and few for the View Model. View Model can have few unit tests that will test the algorithm related stuff like filterCustomers(criteria), sortCustomers(sortOrder). And the UITest can be E2E tests that tests the whole story (expected behavior) and few edge cases.john doe– john doe2023年02月14日 23:57:05 +00:00Commented Feb 14, 2023 at 23:57