XState and Stately core engineer here. In your case, it depends on if you want to test the guard itself as a separate unit or want to test a scenario including a state machine.
Unit test the guard on its own
In this case, I'd only pass on relative pieces of information from machine context and incoming event to the guard function to simplify mocking them, specially if TypeScript typing is involved. Plus, it conceptually makes more sense for a standalone guard function to be decoupled from the host state machine.
{
guards: {
isSomething: (context, event) =>
meetsConditionsForSomething({prop1: context.prop1, prop2: event.prop2})
}
}
// test
it('guard works as expected', () => {
const truthyCase = {...}
expect(meetsConditionsForSomething(truthyCase)).toBe(true)
const falsyCase = {...}
expect(meetsConditionsForSomething(falsyCase)).toBe(false)
})
Test an entire scenario, including the state machine and the path in which that particular guard will be executed.
In this case, you don't wanna unit test the machine or a path inside it. You'd want to test the UI consuming the state machine itself and providing interactions that will get that guard executed.
Note: I don't know Angular so here's a React version of the component but the concept is portable.
const machine = createMachine({
initial: "min",
context: { value: 0 },
states: {
min: {
on: {
increment: {
target: "mid",
actions: "incrementValue",
},
},
},
other: {
on: {
decrement: [
{ target: "min", cond: "willBeZero", actions: "decrementValue" },
{ actions: "decrementValue" },
],
increment: {
actions: "incrementValue",
},
},
},
},
});
// Value gets decrements to zero
function willBeZero(value) {
return value === 1;
}
// component
const Component = () => {
const [state, send] = useMachine(machine, {
guards: {
willBeZero: (context) => willBeZero(context.value),
},
});
return (
<>
<p>value is: {state.context.value}</p>
<button onClick={() => send({ type: "decrement" })}>Decrement</button>
<button onClick={() => send({ type: "increment" })}>Increment</button>
</>
);
};
// Test
it("should start with zero, get to one and come to back to zero", () => {
render(<Component />);
expect(screen.getByText("value is: 0")).toBeInDocument();
screen.getByRole("button", { name: /increment/ }).click();
expect(screen.getByText("value is: 1")).toBeInDocument();
// this is the scenario that gets the guard executed
screen.getByRole("button", { name: /decrement/ }).click();
expect(screen.getByText("value is: 0")).toBeInDocument();
});