Testing United Conference: Unleashing the Power of AI in QA
QA Engineer Iana and her colleague Sara report their insights from the latest Testing United Conference with the main topic “AI Augmented QA".
17.10.2022
written by Holly Smith
The backend team of AURENA Tech is building a new microservice for our customer service staff. The backend is complicated because several services coordinate with each other.
We had a section of work that would be ideal for experimenting with a state machine implementation. It is a pattern we could then take to build larger, more complicated systems in the future. So I was tasked with implementing the state machine inside a micro-service to handle the complaint management workflow for lot distribution.
A Lot Distribution Complaint (LDC) is a record of a refusal to accept a specific distributed lot by the person collecting the lot during a distribution day. The design specification for the workflow of an individual complaint record was written as a state machine (using a MermaidJS plugin in our Confluence Cloud), with the state transitions clearly defined and state entry behaviour specified:
We decided to use an actual state machine implementation in the codebase to explicitly model these state transitions and states as specified.
We evaluated several existing JavaScript state machine libraries and alternatively considered writing our own simple state machine engine. Our selection criteria included:
Using the above criteria we evaluated that XStateJS fulfilled our requirements. Because the Complaints workflow is just a simple finite state machine with no nested or parallel state, could have just done an implementation using a switch statement. However, we also wanted to use this as a proof of concept so we can handle more complicated workflows in the near future.
More on this: Introduction to state machines and statecharts
The JSON below is our implementation of the Complaints statechart in XStateJS.
The states
object lists the states and state transitions, taken directly from the design specification. We love how this code maps directly to the specification. It is immediately obvious if this is complete or if we forgot to include a state transition in the implementation. If we implemented this by a series of unconnected, tangled methods, we would have to repeatedly follow the convoluted code path manually to check this.
As we use TypeScript in our projects, we strongly typed the state machine. To do this we used the XState Typegen library as it automatically generates intelligent typings for XState. This means that the events in the options are “strongly typed to the events that cause the action to be triggered.” (Source: XState – JavaScript State Machines and Statecharts)
createMachine(
{
id: "lot-distribution-complaint",
strict: true,
initial: "idle",
tsTypes: {} as import("./complaint-state-machine.typegen").Typegen0,
schema: {
context: {} as LDCContext,
events: {} as LDCEvent,
},
context: {
aggregate: agg,
},
states: {
idle: {
on: {
LDC_CREATED_BY_BO: { target: "open", actions: ["ldc.created"] },
LDC_CREATED_BY_DW: { target: "open", actions: ["ldc.created"] },
},
},
open: {
on: {
LDC_RESOLVED_BY_BO: { target: "resolved", actions: ["ldc.resolved"] },
LDC_COMMENTED_BY_BO: { target: "open", actions: ["ldc.commented"] },
LDC_DELETED_BY_DW: { target: "deleted", actions: ["ldc.deleted"] },
LDC_REPLACED_BY_DW: { target: "replaced", actions: ["ldc.replaced"] },
},
},
deleted: {
type: "final",
},
resolved: {
on: {
LDC_REOPENED_BY_BO: { target: "open", actions: ["ldc.reopened"] },
},
},
replaced: {
type: "final",
},
},
},
)
The above diagram shows a visual representation of the state machine JSON. It was generated using the XState Visualizer. It is similar to the diagram we use for our requirements and specifications, again making it easy to spot if we have got it right or not early on in the process.
The actual business logic for each state transition is encapsulated in a series of pure XState Action functions, which are easy to mock and unit test individually.
Mostly, the Complaint action functions simply build and emit an Eventicle Event to be emitted (via Kafka). This enables other Eventicle components to consume these events and perform further work.
One gotcha we hit was the realisation that XState Action functions are synchronous; they cannot block on promise-returning method calls (e.g. using “await” ). Sometimes we needed to call async methods to enrich the XState event data (e.g. from additional data in the database). The canonical XState way of handling promises in action functions is to implement the promise as a nested series of pending, fulfilled and rejected substates. We felt this would quickly become too complex especially if there were two or more sequential async calls needed in one action. After some experimentation, we decided to ensure the XState event included all the data needed to raise the emitted Eventicle Events, removing the need to do the enrichment work in the action functions themselves. We may revisit this in the future.
Action example – Add a comment to an OPEN LDC
export function ldcCommentedAction(
context: LDCContext,
event: XStateCommentedByBOEvent,
actionMeta: ActionMeta
) {
let data: ProjectLDCCommentDataV1 = {
commentNote: event.commentNote,
commentedByBOUserId: event.commentedByBOUserId,
complaintState: getComplaintStateFromXStateState(actionMeta.state),
};
context.aggregate.raiseEvent({
type: LotDistributionComplaintEventTypes.COMMENTED,
data,
} as ProjectLDCCommentV1);
}
Upon receiving an LDC_COMMENTED_BY_BO
state, the state machine triggers a ldcCommentedAction
function. The XState event data is then converted to an Eventicle event, which is then emitted.
it("can comment on an OPEN complaint", async () => {
//SETUP
const ldcId = await LotDistributionComplaintCommand.CREATE_BY_DW(
"btdaId",
"btLotId",
"complaintCode",
false,
Date.now(),
null,
null,
"i-am-dw"
);
// EXECUTE
await LotDistributionComplaintCommand.COMMENT_BY_BO(ldcId, "comment note", "9977");
// VERIFY
const events = await consumeFullEventLog(EventStream.LDC);
expect(events.map((value) => value.type)).toStrictEqual([
LotDistributionComplaintEventTypes.CREATED,
LotDistributionComplaintEventTypes.COMMENTED,
]);
expect(events[0].data).toStrictEqual(createCommandMock);
expect(events[1].data).toStrictEqual(commentCommandMock);
});
The above example of a unit test for the state machine tests that a comment can be added to the complaint. A Lot Distribution Complaint (LDC) is created in the setup, by emitting the CREATE_BY_DW
Eventicle command. Once that has finished emitting, then the COMMENT_BY_BO
Eventicle command is executed. Both Eventicle events are received by the state machine and processed. To verify this, the Eventicle event log is checked to see that the expected events were published. The data of the events are also checked.
In conclusion, we successfully implemented our system’s first state machine integrated with Eventicle. We will be taking what we have learnt from this to build larger and more complicated systems.
Interested in working with AURENA Tech? Take a look at the open positions.