I have just migrated from using ObservableObject
and the @Published
property wrapper to the new @Observable
macro in Swift. I am wondering how I can rewrite these kind of tests. Please keep in mind that my tests cover arbitrarily complex cases with asynchronous behavior, and this is just an oversimplified example.
func testVoiceListUpdates() {
let initialExpectation = XCTestExpectation(description: "Voice list should be empty initially")
let firstExpectation = XCTestExpectation(description: "First voice should be added")
let secondExpectation = XCTestExpectation(description: "Second voice should be added")
let viewModel = MyViewModel()
let firstVoice = Voice.fixture()
let secondVoice = Voice.fixture()
viewModel.$voiceList
.sink { newValue in
switch newValue.count {
case 0:
initialExpectation.fulfill()
case 1:
if newValue.first == firstVoice {
firstExpectation.fulfill()
viewModel.addVoice(secondVoice)
}
case 2:
if newValue.last == secondVoice {
secondExpectation.fulfill()
}
default:
break
}
}
.store(in: &cancellables)
viewModel.addVoice(firstVoice)
wait(for: [initialExpectation, firstExpectation, secondExpectation], timeout: 1)
}
The point is that in the MVVM architecture all of the logic is in the ViewModel, and all of the visible behavior can be fully tested by just testing the ViewModels. How are we supposed to do this with the new @Observable
macro? Any suggestions and best practices would be appreciated.
Introducing a Dedicated Testing
CurrentValueSubject
when using@Observable
A simple solution would be to introduce a
testVoiceListSubject
in our ViewModel to observe changes in thevoiceList
property. Below is how we can adjust our ViewModel and test class accordingly:This approach works great because we get all the simplicity and performance benefits from the new
@Observable
macro, since thisCurrentValueSubject
doesn't have any side effects like@Published
had, and it requires minimal setup. We assume that someday Apple will add a way to listen to properties in classes that use the@Observable
macro directly, which could provide a more integrated solution. For now, this method allows us to retain the testability of our ViewModel while adapting to the new@Observable
macro, ensuring a smooth transition as we leverage the new features in Swift.