square/workflow-swift

Change to a chain-able RenderTester API

bencochran opened this issue · 3 comments

As described in square/workflow#702, we should move to a chain-able RenderTester API.

Benefits:

  1. Separate expectations during the render from verification of post-render state of the world

    Our current ExpectedState is the trickiest of this IMO. It’s unclear from the API if this is asserts the state before the render or after it (it’s after).

  2. Extensibility

    The current model, all expectations being passed in a single method, leaves us in an extensibility trap. When Workers become Workflows, it’s natural that ExpectedWorker is actually a wrapper around ExpectedWorkflow. We can do that from the same module without breaking the API, but if we move Worker out of the core module, we have to move worker expectations with it. By making expectations expressed as method calls, we can move the expect(worker:…) method to an extension and build it on top of expectWorkflow(type:…).

  3. Personally, I think it reads nicer

    A chained API reads more easily as a story down a single column than what we have today where the actual types being expected are nested a few layers deep in a single call. (It also throws all of its implementation detail onto the API surface)

Example

NameLoadingWorkflow()
    .renderTester(initialState: .init(state: .loading, token: "user-token"))
    .expect(
        worker: LoadingWorker(token: "user-token"),
        producingOutput: .success("Ben Cochran")
    )
    .expectSideEffect(key: "some-extra-side-effect-just-for-fun")
    .render { rendering in 
        XCTAssertEqual(rendering.title, "Loading")
    }
    .verify(
        action: MyWorkflow.Action.loadSuccess(name: "Ben Cochran")
    )
    .verify(
        output: .complete
    )

Open Questions

  1. Today, Swift’s RenderTester can be used across multiple renders (it updates its internal workflow state according to the actions produced in each render). Kotlin’s RenderTester does not allow this. We should probably decide which model we like and converge.

  2. Our WorkflowActionTester uses assert in its method names. Kotlin’s RenderTester uses verify. We should probably consolidate all of these to either assert or verify across the board.

  1. The Kotlin API intentionally does not allow reuse to encourage small, self-contained tests that test one thing.

  2. The Kotlin API does not use assert in the name because, for the most part, the methods themselves do not actually assert anything. They accept a lambda in which you can write actual assertions.

The methods that actually assert on values themselves (that you implemented but kotlin hasn't yet) I would be fine naming assert.

  1. Yeah, I think not allowing chaining multiple renders is good. As written in this PR, the old functionality still exists in the deprecated method (by returning a whole new RenderTester), but the new API is a one-shot immutable model.

  2. Oh, yeah, that’s a good distinction. In general I like verify for the ones where the consumer asserts and assert for the ones where the tester asserts. We do though have a couple of these that do a little bit of both (e.g. verifyOutput takes an (Output) -> Void and first asserts that there is an output before passing it to the given assertion closure), but I’m inclined to keep them as verify.