Unit Test @Binding properties

I would like to implement Unit Tests for @Binding properties in a ViewModel.

Sample project can be downloaded here : https://github.com/raphaelguye/BindingTester

Basically the ViewModel looks like :

class SheetViewModel: ObservableObject {

  @Binding var isPresented: Bool
  
  func onClose() {
    isPresented = false // To be unit tested
  }
  
  init(isPresented: Binding<Bool>) {
    _isPresented = isPresented
  }
}

And I'm trying to make unit test in different ways (no one are working) :

func test1() {
  let viewModel = SheetViewModel(isPresented: .constant(true))

  XCTAssertTrue(viewModel.isPresented)
  viewModel.onClose()
  XCTAssertFalse(viewModel.isPresented) // remains true...
}
func test2() {
  @State var isSheetDisplayed: Bool = true
  let viewModel = SheetViewModel(isPresented: $isSheetDisplayed) // Accessing State's value outside of being installed on a View.

  XCTAssertTrue(viewModel.isPresented)
  viewModel.onClose()
  XCTAssertFalse(viewModel.isPresented) // remains true...
}
func test3() {
  let isSheetDisplayed: Binding<Bool> = .constant(true)
  let viewModel = SheetViewModel(isPresented: isSheetDisplayed)

  XCTAssertTrue(viewModel.isPresented)
  viewModel.onClose()
  XCTAssertFalse(viewModel.isPresented) // remains true...
}

Ah the use of .constant really means a Binding whose value cannot change. So the fact that the value remains true is expected.

And unfortunately you really can't use @State outside of Swift UI. There's a lot of machinery within the property wrapper that doesn't update what it needs to unless it is participating in a View hierarchy.

From what you've posted here it looks like you want to unit test that calling onClose() on your view model calls the setter of the binding with false. You could unit test this by passing in a binding that sets a local flag like so.

var bindingValue = false
let binding = Binding(get: { bindingValue }, set: { bindingValue = $0 })

Then, check the value of your local variable bindingValue after calling onClose().

1 Like

I was under the impression that @Binding was for views, and @Published was for view models. Switching to @Published might make things a bit more easy to test.

3 Likes

This may only be tangential to what you're asking but I've been using this library: GitHub - nalexn/ViewInspector: Runtime introspection and unit testing of SwiftUI views to do a lot of my SwiftUI testing. It's super helpful when you need your view hosted to get that validation you want without relying on a slow XCUI test.

1 Like

You are totally right, but if I have to manage the isPresented variable as a @Published property in the SheetViewModel, I have to be register on its change on the SheetView to update the Binding value (received from the RootView)..

I did it first, to avoid import SwiftUI in my ViewModels, but it looks like an overkill having to do it for each Binding values...

What do you think?
Or do you have another idea ?

Thanks it's working !
It makes the job I need.

Sorry, without more code demonstrating the problem, I'm not sure I understand, but if you can flesh out an explanation for the problem you're facing I'll give it a shot!

@stephencelis : I have created a specific branch feature/vm-without-swiftui on my repo for this use case.

As you can see in this commit, I have removed the SwiftUI package from the ViewModel but I had to add a lot of code in the View to satisfy the Binding. Which I don't like all..

Do you have another suggestion?

Terms of Service

Privacy Policy

Cookie Policy