Assertions
If you’ve done testing before, you might be familiar with the setup, then assert pattern for writing tests:
- Setup to get the app into the desired state.
- Assert by checking the app is in the correct state.
The following example shows a whole test for a function that adds a user to a database:
ts
// PSEUDOCODEdescribe("addUserToDb", () => {it("Should add a user to a database", async () => {// SETUP// Clear the database before each testawait testUtils.clearDatabase();// Run the function we’re testingawait addUserToDb({name: "Matt",});// ASSERT// Ensure that the user existsawait testUtils.ensureUserExistsInDb({name: "Matt",});});});
ts
// PSEUDOCODEdescribe("addUserToDb", () => {it("Should add a user to a database", async () => {// SETUP// Clear the database before each testawait testUtils.clearDatabase();// Run the function we’re testingawait addUserToDb({name: "Matt",});// ASSERT// Ensure that the user existsawait testUtils.ensureUserExistsInDb({name: "Matt",});});});
The test above follows the setup then assert steps:
- Setup: Clear the database, then call the
addUserToDb
function we’re testing. - Assert: Check the app is in the correct state with
ensureUserExistsInDb
.
Setup
You have two main options to run test setup in @xstate/test
.
Setup before each test path
If your setup needs to run before each test path, run the setup just before calling path.test()
:
ts
const paths = model.getPaths();describe("My model", () => {paths.forEach((path) => {it(path.description, () => {/*** Run any setup that needs to happen* before each test*/// Run the testpath.testSync({states: {},events: {},});/*** Run any teardown that needs to* happen after each test*/});});});
ts
const paths = model.getPaths();describe("My model", () => {paths.forEach((path) => {it(path.description, () => {/*** Run any setup that needs to happen* before each test*/// Run the testpath.testSync({states: {},events: {},});/*** Run any teardown that needs to* happen after each test*/});});});
Setup during a test
If your setup needs to happen during a test, you pass an implementation to an event. The following example tests a button. The button’s text begins as ‘pending,’ but the text turns into ‘complete’ after the button is clicked.
ts
const machine = createTestMachine({initial: "buttonIsPending",states: {buttonIsPending: {on: {CLICK: "buttonIsComplete",},},buttonIsComplete: {},},});createTestModel(machine).getPaths().forEach((path) => {it(path.description, () => {path.testSync({events: {CLICK: () => {cy.findByRole("button").click();},},});});});
ts
const machine = createTestMachine({initial: "buttonIsPending",states: {buttonIsPending: {on: {CLICK: "buttonIsComplete",},},buttonIsComplete: {},},});createTestModel(machine).getPaths().forEach((path) => {it(path.description, () => {path.testSync({events: {CLICK: () => {cy.findByRole("button").click();},},});});});
When the testModel
wants to know how to implement the CLICK
event, it looks inside events
and runs the function. The result is that the model knows how to setup the app in each state:
- The test model knows how to setup the button in the
buttonIsPending
state as it’s the machine’s initial state. - The test model knows how to setup the
buttonIsComplete
state by running theCLICK
event.
Assert
Once your model can set up your app in each state, you should assert that your app is actually in that state. You can do this by passing states
to path.test
:
ts
createTestModel(machine).getPaths().forEach((path) => {it(path.description, () => {path.testSync({events: {CLICK: () => {cy.findByRole("button").click();},},states: {buttonIsPending: () => {cy.findByRole("button").should("have.text","pending",);},buttonIsComplete: () => {cy.findByRole("button").should("have.text","complete",);},},});});});
ts
createTestModel(machine).getPaths().forEach((path) => {it(path.description, () => {path.testSync({events: {CLICK: () => {cy.findByRole("button").click();},},states: {buttonIsPending: () => {cy.findByRole("button").should("have.text","pending",);},buttonIsComplete: () => {cy.findByRole("button").should("have.text","complete",);},},});});});
The test model can now ensure that the app is in the state as described by the machine.
This approach is a good general guide for testing with @xstate/test
:
- Run assertions in states
- Run setup in events, or before/after
path.test
.