Skip to main content

Writing state handlers

Like triggers, not all example types need state handlers during contract definition. For example, if your service under test is an HTTP client, the default state variables are used and you don't need to define state handlers. If your service under test is an HTTP server, you will need to provide state handlers.

State handlers are defined with the stateHandlers key on the ContractCaseConfig object. The stateHandlers object is keyed by the state name.

When are state handlers invoked?

State handlers are invoked by ContractCase during setup for Examples that were defined with states (see the ContractCase Example Lifecycle). The order of execution of state handlers is the same as the order that the states were defined in.

If you haven't defined states first, read the section on defining states, which must be done during contract definition.

What should a state handler do?

State handlers are invoked to set some preconditions for your service. This means that each Example is able to be independent, allowing your tests to be specific and easily repeatable.

A state handler is a function that is able to modify the state of a running service. This is usually done by mocking the repository layer of your service, but it can also be done by inserting into a database, or mocking at the equivalent of the controller layer of your service. For more discussion, see the section on best practices for state handlers.

Optionally, a state handler can provide a teardown function too. This is provided in case you need to undo the precondition setup (removing records from the database, changing the mock back to a default, etc)

Setup state handlers

A setup state handler is is only invoked during the state setup lifecycle phase.

It can be defined as simple function:

stateHandlers: {
'Server is up': () => {
// Configure your running server to appear to be up here
},
},

Or as a setup key on the stateHandler value:

stateHandlers: {
'Server is up': {
setup: () => {
// Configure your running server to appear to be up here
},
}
},

If your function needs to be asynchronous, you can return a Promise or make it async.

stateHandlers: {
'Server is up': {
setup: async () => {
// Configure your running server to appear to be up here
},
}
},

Returning variables from state setup

If the state has variables, your state setup function must return an object keyed by variable name, where each value is the variable value. You must provide a value for all variables, and those values must match any Test Equivalence Matchers set on the variable when it was set up.

stateHandlers: {
// assuming inState('A user exists', { userId: 'foo' })
'A user exists': async () => {
const userId: string = await insertUser({ .... });
return { userId };
},

// .... etc
},

State teardown functions

State teardown functions are run after the Example has completed execution (see the Example Lifecycle documentation for more information).

State teardown functions are specified with the teardown key on a state setup object:

stateHandlers: {
'A user exists': {
setup: async () => {
const userId: string = await insertUser({ .... });
return { userId };
},
teardown: async () => {
await removeUsers();
}
}
},

State teardown functions do not need to return anything.