Is it possible to assert Snapshot in between TestStore .send and .receive asserts?

Hi,

Is there a way to do SnapshotTesting by using the .do function when asserting on the TestStore in between .send and .receive ?

    testStore.assert([
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "initialState") },
      .send(.mapContainer(.mapView(.locationManager(.didUpdateLocations([parisLocation]))))) {
        $0.mapContainer.mapView.userLocationCoordinate = parisLocation
      },
      .receive(.mapContainer(.mapView(.centerOnUserReceived))) {
        $0.mapContainer.mapView.mapCenter = parisLocation.coordinate
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterDidUpdateLocations(\(parisLocation)") },
      .send(.mapContainer(.floatingButtonView(.searchAgainTapped))),
      .receive(.refreshItemsFromMapCoordsReceived),
      .receive(.overlayContainer(.overlayLocal(.updateLevelReceived(.minimum)))),
      .receive(.mapContainer(.floatingButtonView(.hideSearchAgainReceived(true)))),
      .receive(.overlayContainer(.overlayLocal(.refreshPositionReceived))) {
        $0.overlayContainer.overlayLocal.position.height = -60
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterSearchAgainTapped") },
      .send(.mapContainer(.mapView(.userTappedPOI(selectedPro)))) {
        $0.selectedPro = selectedPro
        $0.content = .fd
      },
      .receive(.overlayContainer(.overlayLocal(.updateLevelReceived(.half)))) {
        $0.overlayContainer.overlayLocal.level = .half
      },
      .receive(.mapContainer(.mapView(.centerOnSelectedProReceived))) {
        $0.mapContainer.mapView.mapCenter = selectedPro?.coordinates
      },
      .receive(.overlayContainer(.overlayLocal(.refreshPositionReceived))) {
        $0.overlayContainer.overlayLocal.position.height = 0
      },
      .do { assertSnapshot(matching: view, as: .windowedImage, named: "afterUserTappedPOI\(selectedPro)") },
    ])

For this to work we need TestStore to provide a way to return a proper Store and feed TestStore to the View.

1 Like

I would love to see something like this baked right in. But for now, how about an extension like this?

import SwiftUI
import ComposableArchitecture
import SnapshotTesting

extension TestStore.Step {
    static func receive<V: View>(
        _ action: Action,
        view: @escaping (Store<LocalState, LocalAction>) -> V,
        _ update: @escaping(inout LocalState) -> Void
    ) -> Self {
        .receive(action, { state in
            let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
            let view = view(store)
            assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
            update(&state)
            
        })
    }
    
    static func send<V: View>(
        _ action: LocalAction,
        view: @escaping (Store<LocalState, LocalAction>) -> V,
        _ update: @escaping(inout LocalState) -> Void
    ) -> Self {
        .send(action, { state in
            let store = Store<LocalState, LocalAction>.init(initialState: state, reducer: .empty, environment: ())
            let view = view(store)
            assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone8Plus)))
            update(&state)
            
        })
    }
}

I would love to see if we can improve ergonomics on this function even more. An obvious start would be to parameterize the Snapshotting strategy.

2 Likes

Hey Luke, this solution works great. I think I've made a few improvements:

  • Passing in parameters for #line, #function, and #file improves the test output in Xcode and helps organize the snapshots
  • Calling update(&state) first ensures that the state change is applied before the snapshot is taken.
  • Calling assertSnapshot in a DispatchQueue.main.async block seems to speed up the tests by an order of magnitude (100-300ms down to 10-20ms). I think this is because instead of blocking at each step, the snapshots are deferred until the next iteration of the run loop and executed as a batch.
Terms of Service

Privacy Policy

Cookie Policy