Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Ibé Dwi
Ibé Dwi

Posted on • Edited on

     

Testing XState FSM using Jest

After contributing to an open-source project, I've come to understand the importance of writing tests. One advantage of projects with tests is that we can quickly verify if existing features break when changes are made. In fact, when developing a feature, we can swiftly ensure it works as intended for given cases through testing.

This benefit is also true when writing finite state machines (FSM) using XState. Questions arose in my mind, "What if we could quickly verify our FSM works while we're creating it?" and "Does adding new features disrupt existing functionalities?"

In this article, I'll share how I incorporate testing when developing FSMs.

Before continuing, as a disclaimer, XState also offers a package for writing tests. Even more advanced, this package can automatically generate test cases. However, this article focuses on how I test the FSMs I've created against our own (imperative) test cases.

The application discussed in this article can be viewed inthis repository.


Case Study

For me, one of the most exciting ways to learn is by using case studies.

In this article, I will use the "phone keypad" as a case study. For those who are unfamiliar, a "phone keypad" is a type of "keyboard" found on older mobile phones.

Nokia 3310

(Sourcehttps://www.gsmarena.com/nokia_3310-pictures-192.php)

Some functionalities we aim to achieve:

  1. Pressing a button for the first time will select the character group on that button and choose the first character in the group.

  2. Repeatedly pressing the same button will select characters according to their order.

  3. If no button is pressed after a set time, the currently selected character will be inserted into the text.

  4. Pressing a different button will insert the currently selected character into the text and change the selection to the first character on the newly pressed button.


Preparing the Project

Next.js

I am using Next.js 13 with the app directory and TailwindCSS. A tutorial on creating a Next.js project with the app router can be found at this link.

What is your project named? logs-understanding-fsm-with-xstateWould you like to use TypeScript? YesWould you like to use ESLint? YesWould you like to use Tailwind CSS? YesWould you like to use `src/` directory? YesWould you like to use App Router? (recommended) YesWould you like to customize the default import alias (@/*)? YesWhat import alias would you like configured? @/*
Enter fullscreen modeExit fullscreen mode

xstate &@xstate/react

In this article, I am using XState version 4.

yarn add xstate@^4.38.1 @xstate/react@^3.2.2
Enter fullscreen modeExit fullscreen mode

jest &ts-jest

Install the following libraries:

 yarn add-D jest ts-jest @types/jest
Enter fullscreen modeExit fullscreen mode
  • jest: this library is used for testing.

  • ts-jest: this library allows us to directly run tests written in TypeScript without having to transpile to JS.

  • @types/jest: this is the type definition for jest.

Then, execute the following line to initialize Jest configuration with thets-jest preset.

yarn ts-jest config:init
Enter fullscreen modeExit fullscreen mode

Folder Structure

Here is the folder structure:

next-app/├─ src/│  ├─ app/│  │  ├─ phone-keypad/│  │  │  ├─ page.tsx│  │  ├─ ...│  ├─ features/│  │  ├─ phone-keypad/│  │  │  ├─ constant.ts│  │  │  ├─ phoneKey.fsm.spec.ts│  │  │  ├─ phoneKeypad.fsm.ts│  │  │  ├─ phoneKeypad.module.css│  │  │  ├─ PhoneKeypad.tsx│  │  ├─ ...│  ├─ ../├─ ...
Enter fullscreen modeExit fullscreen mode

.fsm are files that contain the definition and the test of our FSM

PhoneKeypad is a component that implements the state machine we will create and integrates it with the UI.

phone-keypad/page.tsx is the page where we display the created keypad.


Layering the application

Separating an application into distinct layers according to their responsibilities makes it easier to maintain. This principle is known as "separation of concern." In this article, I have divided the application into two layers: the UI layer (presentation layer) and the domain layer.

The UI Layer is the layer that consists of displays, such as web pages or components. The domain layer, on the other hand, is the layer that contains the business logic, which in this case is the FSM.

Domain Layer

Representation of the keypad

The first thing I did was create a representation of the keypad to be displayed. Based on the functionality criteria above, the keypad will be pressed one or several times to obtain the desired character. For example, on a Nokia 3310 phone, the number 2 key consists of 3 alphabets and 1 number:

'abc2'
Enter fullscreen modeExit fullscreen mode

To get the letter "b", I have to press the button twice. The first press will display the letter "a", and the second press within a certain period will display the letter "b".

There are at least two alternatives that I have thought of to represent the existing keys:

Using array of string (1-dimensional array)

exportconstPHONE_KEYS=['1','ABC2','DEF3','GHI4','JKL5','MNO6','PQRS7','TUV8','WXYZ9','*',' 0','#']
Enter fullscreen modeExit fullscreen mode

or, as array of characters (2-dimensional array)

exportconstPHONE_KEYS=[["1"],["A","B","C","2"],["D","E","F","3"],["G","H","I","4"],["J","K","L","5"],["M","N","O","6"],["P","Q","R","S","7"],["T","U","V","8"],["W","X","Y","Z","9"],["*"],["","0"],["#"],];
Enter fullscreen modeExit fullscreen mode

Here, I am using the first option. There's no specific reason; it's just a personal preference.

Later on, this array can be used to create the keypad display. It would look something like this:

Illustration of the mapping

From the illustration of the mapping above, we can use the expressionPHONE_KEYS[characterGroupIndex][characterIndex] to refer to a character

Finite State Machine

Considering the focus of this article is on how I test the FSM I created, I have already developed the FSM to be used.

Here is the context that will be used by this FSM:

exporttypeMachineContext={currentCharacterGroupIndex?:number;currentCharacterIndex?:number;lastPressedKey?:number;str:string;};
Enter fullscreen modeExit fullscreen mode

currentCharacterGroupIndex is used to select a character group, andcurrentCharacterIndex is used to select a character within that group. For example, to refer to the character “A”, the value ofcurrentCharacterIndex would be 0. To refer to the character “B”, the value used would be 1, and so on.

Illustration of character selection

lastPressedKey is used to track the last pressed button. Lastly,str is used to store the text that we type.

The FSM recognizes one type of event, which are:

exporttypeMachineEvent=|{type:"KEY.PRESSED";key:number;}
Enter fullscreen modeExit fullscreen mode

The event "KEY.PRESSED" informs the FSM that a button has been pressed. This event carries a "key" property that tells the machine which character group to use.

The overall behavior of the FSM can be seen in the following diagram:

Overall FSM

The first functionality,

Pressing a button for the first time will select the character group on that button and choose the first character in the group.

is fulfilled when transitioning from the state "Idle" to "Waiting for key being pressed again". In the event that is sent, "KEY.PRESSED", there is an action namedonFirstPress which will change the value ofcurrentCharacterGroupIndex to the "key" carried by the "KEY.PRESSED" event andcurrentCharacterIndex to 0. In this action, we also store the "key" carried in thelastPressedKey property as a reference for the second functionality.

First functionality FSM

(Where the first functionality is fulfilled)

The second functionality,

Pressing the same button repeatedly will select characters in sequence.

is fulfilled when the state "Waiting for key being pressed again" receives the "KEY.PRESSED" event but the guard "isTheSameKey?" is satisfied. The "isTheSameKey?" guard checks whether the "key" brought by the "KEY.PRESSED" event is the same as thelastPressedKey property stored in the context. If satisfied, theonNextPress action on the event will be called. This action will increment the value ofcurrentCharacterIndex. If the value ofcurrentCharacterIndex reaches the last character, it will reset to 0.

Second functionality FSM

(Where the second functionality is fulfilled)

The third functionality,

If no button is pressed after a set time, the currently selected character will be inserted into the text.

is fulfilled when no "KEY.PRESSED" event is received within500ms while in the "Waiting for key being pressed again" state. In other words, the FSM will wait for500ms before triggering the "after 500ms" event. The state will then transition to "Waited time passed" which subsequently transitions to the "Idle" state and triggers the actions "assignToString" and "removeSelectedKey" in sequence.

The "assignToString" action will add the character atcurrentCharacterGroupIndex andcurrentCharacterIndex to the contextstr. The "removeSelectedKey" action will clear the values ofcurrentCharacterGroupIndex,currentCharacterIndex, andlastPressedKey.

It is important to note that in the "Waiting for key being pressed again" state, if a "KEY.PRESSED" event is received, the500ms wait time will reset from0.

Third functinoality in FSM

(Where the third functionality is fulfilled)

The final functionality,

Pressing a different button will insert the currently selected character into the text and change the selection to the first character on the newly pressed button.

is fulfilled when in the "Waiting for key being pressed again" state, a "KEY.PRESSED" event is received but does not satisfy the guard "isTheSameKey?". This event will trigger the actions "assignToString", "removeSelectedKey", and "onFirstPress". Notably, the first two actions in this event are the same as when we add the currently referenced character to the string. Meanwhile, the "onFirstPress" action, which is last in the sequence, will update the propertiescurrentCharacterGroupIndex,currentCharacterIndex, andlastPressedKey according to the "key" property brought by the "KEY.PRESSED" event.

Fourth functionality in FSM

(Where the fourth functionality is fulfilled)

The complete definition of the FSM can be viewed in the repository.


Testing the FSM

Finally, we get to the core of this article!

Before I knew about testing, what I did to verify my state machine was to test it directly alongside the UI! However, the more complex my FSM became, the harder it was to test certain states, especially states approaching the final state.

For me, there are two main benefits of testing the FSM I created:

  1. During the development process, I can verify that the FSM I created works as intended without having to touch the UI. Returning to the principle of layering mentioned earlier, the FSM is in the logic layer while the UI is in the UI layer (presentation layer). The UI layer has no connection to the correctness of the logic layer.

  2. If there are changes to the FSM, I can quickly re-verify whether my FSM still works as expected, as outlined in the test cases.

So, what needs to be tested?

For the case in this article, I take the existing functionalities as a reference for testing.

Writing Test Cases

First, I create a test file namedphoneKey.fsm.spec.ts. Then, I add a test suite named “phoneKeypad”:

// phoneKey.fsm.spec.tsdescribe("phoneKeypad",()=>{// test cases will be written in here...})
Enter fullscreen modeExit fullscreen mode

The first test case ensures that functionalities 1, 2, and 3 are met:

constwaitFor=async(time:number)=>newPromise((r)=>setTimeout(r,time));describe("phoneKeypad",()=>{it("should be able to type using the keys",async()=>{constfsm=interpret(phoneKeypadMachine)fsm.start();fsm.send({type:"KEY.PRESSED",key:0});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("1");fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("12");});})
Enter fullscreen modeExit fullscreen mode

In the expression:

constfsm=interpret(phoneKeypadMachine.withConfig({})).start();
Enter fullscreen modeExit fullscreen mode

We interpret the already createdphoneKeypadMachine using theinterpret function. This function will return a running process based on the FSM we have created. This process is referred to as an “actor”.

As a note, theinterpret function is deprecated in XState5. If you are using XState5, you can use thecreateActor function. (Ref)

The process stored in thefsm variable has not yet started. To run it, we can use thestart method.

fsm.start();
Enter fullscreen modeExit fullscreen mode

Next, we simulate a button being pressed.
Before defining the test suite, we define a function namedwaitFor. This function is used to wait for a certain amount of time.

In these statements:

fsm.send({type:"KEY.PRESSED",key:0});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("1");
Enter fullscreen modeExit fullscreen mode

We send the "KEY.PRESSED" event with key 0 to the state machine. Referring to the buttons we have created:

exportconstPHONE_KEYS=['1','ABC2','DEF3','GHI4','JKL5','MNO6','PQRS7','TUV8','WXYZ9','*',' 0','#']
Enter fullscreen modeExit fullscreen mode

the selected character is “1”. We then wait for500ms. We check if the value ofcontext.str is “1”. This set of statements tests functionalities 1 and 3.

Then, in the following statements:

fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(200);fsm.send({type:"KEY.PRESSED",key:1});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("12");
Enter fullscreen modeExit fullscreen mode

We send the "KEY.PRESSED" event four times with key “1”. To ensure that a character is only added after500ms, each time we press the button with key “1”, we wait for200ms. We know that the button with key “1” refers to the second character group,'ABC2'. Pressing it four times before500ms are up will select the fourth character, which is “2”.

At the end of these statements, we test whether the character “2” has been added to the existingstr, so the current value ofstr should now be “12”.

Finally, we test the fourth functionality:

it("pressing different key will added the current key to string",async()=>{constfsm=interpret(phoneKeypadMachine).start();fsm.send({type:"KEY.PRESSED",key:0});fsm.send({type:"KEY.PRESSED",key:1});fsm.send({type:"KEY.PRESSED",key:2});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("1AD");});
Enter fullscreen modeExit fullscreen mode

What we do in the above test case is more or less the same as what we did in the previous test case. We interpret the FSM, start the actor from the FSM, and send events according to the functionality requirements.

We can run the test using Jest. First, open the terminal and run the following command:

yarn test phoneKey.fsm
Enter fullscreen modeExit fullscreen mode

If everything goes smoothly, your terminal should display the following message:

 PASS  src/features/phoneKeypad/phoneKey.fsm.spec.ts  phoneKeypad    ✓ should be able to type using the keys (1628 ms)    ✓ pressing different key will added the current key to string (508 ms)Test Suites: 1 passed, 1 totalTests:       2 passed, 2 totalSnapshots:   0 totalTime:        3.962 s
Enter fullscreen modeExit fullscreen mode

All functionalities have been tested solely through the state machine! Now, it's time to integrate the FSM with the UI!


Integrating FSM into UI

I created a file namedPhoneKeypad.tsx which will later be imported into a Next.js page.

Here is the UI component without integration with FSM:

"use client";import{useInterpret,useSelector}from"@xstate/react";import{PHONE_KEYS}from"./constant";exportfunctionPhoneKeypad(){return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4"><divclassName="w-full border border-solid col-span-3 h-[50px] max-w-[332px] flex justify-start items-center px-4"><pclassName="max-w-full overflow-x-auto flex-nowrap whitespace-nowrap">            Text will be displayed here...{/* TODO: Add current str value */}{/* TODO: Add current selected character preview */}{/* TODO: Add blinking caret */}</p></div>{PHONE_KEYS.map((key,index)=>(<buttonkey={index}className={["w-[90px] h-[90px] rounded-lg bg-gray-100 flex flex-col justify-center items-center","hover:bg-gray-200 cursor-pointer active:bg-gray-300",].join("")}style={{userSelect:"none",}}// TODO: add "KEY.PRESSED" eventonClick={()=>{}}><pclassName="text-2xl">{key[key.length-1]}</p><divclassName="gap-1 flex"><span>{key}</span></div></button>))}</div></div>);}
Enter fullscreen modeExit fullscreen mode

The above code snippet includes placeholders for the text input by the user and buttons that will send the "KEY.PRESSED" event.

Integrating Machine

First, we import hooks to integrate XState into a React component. Here I useuseInterpret anduseSelector. In XState 4,useInterpret is a hook that returns an “actor” or “service” based on the given state machine. Unlike theinterpret used in the test, the “service” returned here automatically starts and runs for the lifetime of the React component.

useInterpret returns a static reference from the FSM to the React component used only to interpret the FSM. UnlikeuseMachine, which flush all updates to the React component causing re-renders on every update, updates to the FSM used byuseInterpret will not cause the React component to re-render.

Then how do we get the latest state from the FSM? We can use theuseSelector hook to select which parts of the FSM we want to monitor and cause re-renders in our component.

For this case, there are at least 4 things we want to track:

  1. ContextcurrentCharacterGroupIndex andcurrentCharacterIndex to display the currently selected character

  2. Contextstr to display the text that has been created

  3. StateisIdle used to display a “cursor” or “caret” indicating the machine is waiting for input from the user.

Here is how to useuseInterpret anduseSelector:

exportfunctionPhoneKeypad(){// ...constfsm=useInterpret(phoneKeypadMachine);const{currentCharacterGroupIndex,currentCharacterIndex,value,isIdle}=useSelector(fsm,(state)=>({currentCharacterGroupIndex:state.context.currentCharacterGroupIndex,currentCharacterIndex:state.context.currentCharacterIndex,value:state.context.str,isIdle:state.matches("Idle"),}));// ...}
Enter fullscreen modeExit fullscreen mode

Then, we can use thevalue to complete the first TODO, “Add current str value”

"use client";import{useInterpret,useSelector}from"@xstate/react";import{PHONE_KEYS}from"./constant";exportfunctionPhoneKeypad(){return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4"><divclassName="w-full border border-solid col-span-3 h-[50px] max-w-[332px] flex justify-start items-center px-4"><pclassName="max-w-full overflow-x-auto flex-nowrap whitespace-nowrap">{/* TODO: Add current str value */}{value}{/* TODO: Add current selected character preview */}{/* TODO: Add blinking caret */}</p></div>{/* ... */}</div></div>);}
Enter fullscreen modeExit fullscreen mode

The second TODO, “Add current selected character preview”, is completed by adding a character preview usingcurrentCharacterGroupIndex andcurrentCharacterIndex

// ...exportfunctionPhoneKeypad(){// ...return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4"><divclassName="w-full border border-solid col-span-3 h-[50px] max-w-[332px] flex justify-start items-center px-4"><pclassName="max-w-full overflow-x-auto flex-nowrap whitespace-nowrap">{/* TODO: Add current str value */}{value}{/* TODO: Add current selected character preview */}{selectedIndex!=undefined&&selectedIndexElement!=undefined&&(<span>{PHONE_KEYS[selectedIndex][selectedIndexElement]}</span>)}{/* TODO: Add blinking caret */}</p></div>{/* ... */}</div></div>);}
Enter fullscreen modeExit fullscreen mode

Lastly, to indicate that we will add text at the end of the existing text, we can add a caret or cursor when the FSM is in the “Idle” state

// ...importclassesfrom"./phoneKeypad.module.css";// ...exportfunctionPhoneKeypad(){// ...return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4"><divclassName="w-full border border-solid col-span-3 h-[50px] max-w-[332px] flex justify-start items-center px-4"><pclassName="max-w-full overflow-x-auto flex-nowrap whitespace-nowrap">{/* TODO: Add current str value */}{value}{/* TODO: Add current selected character preview */}{selectedIndex!=undefined&&selectedIndexElement!=undefined&&(<span>{PHONE_KEYS[selectedIndex][selectedIndexElement]}</span>)}{/* TODO: Add blinking caret */}<spanclassName={[classes.blinkingCaret,isIdle?"":"hidden"].join("")}>              |</span></p></div>{/* ... */}</div></div>);}
Enter fullscreen modeExit fullscreen mode

I also created a CSS module that will make the “caret” blink every500ms:

/* phoneKeypad.module.css */.blinkingCaret{animation:blink500msinfinite;}@keyframesblink{50%{opacity:0;}}
Enter fullscreen modeExit fullscreen mode

Finally, we send the "KEY.PRESSED" event when a button is pressed:

"use client";import{useInterpret,useSelector}from"@xstate/react";import{PHONE_KEYS}from"./constant";import{phoneKeypadMachine}from"./phoneKeypad.fsm";importclassesfrom"./phoneKeypad.module.css";exportfunctionPhoneKeypad(){// ...return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4">{/* ... */}{PHONE_KEYS.map((key,index)=>(<buttonkey={index}className={["w-[90px] h-[90px] rounded-lg bg-gray-100 flex flex-col justify-center items-center","hover:bg-gray-200 cursor-pointer active:bg-gray-300",].join("")}style={{userSelect:"none",}}// TODO: add "KEY.PRESSED" eventonClick={()=>fsm.send({type:"KEY.PRESSED",key:index})}><pclassName="text-2xl">{key[key.length-1]}</p><divclassName="gap-1 flex"><span>{key}</span></div></button>))}</div></div>);}
Enter fullscreen modeExit fullscreen mode

Here is the complete code snippet:

"use client";import{useInterpret,useSelector}from"@xstate/react";import{PHONE_KEYS}from"./constant";import{phoneKeypadMachine}from"./phoneKeypad.fsm";importclassesfrom"./phoneKeypad.module.css";exportfunctionPhoneKeypad(){constfsm=useInterpret(phoneKeypadMachine);const{selectedIndex,selectedIndexElement,value,isIdle}=useSelector(fsm,(state)=>({selectedIndex:state.context.currentCharacterGroupIndex,selectedIndexElement:state.context.currentCharacterIndex,value:state.context.str,isIdle:state.matches("Idle"),}));return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4"><divclassName="w-full border border-solid col-span-3 h-[50px] max-w-[332px] flex justify-start items-center px-4"><pclassName="max-w-full overflow-x-auto flex-nowrap whitespace-nowrap">{value}{selectedIndex!=undefined&&selectedIndexElement!=undefined&&(<span>{PHONE_KEYS[selectedIndex][selectedIndexElement]}</span>)}<spanclassName={[classes.blinkingCaret,isIdle?"":"hidden"].join("")}>              |</span></p></div>{PHONE_KEYS.map((key,index)=>(<buttonkey={index}className={["w-[90px] h-[90px] rounded-lg bg-gray-100 flex flex-col justify-center items-center","hover:bg-gray-200 cursor-pointer active:bg-gray-300",].join("")}style={{userSelect:"none",}}onClick={()=>fsm.send({type:"KEY.PRESSED",key:index})}><pclassName="text-2xl">{key[key.length-1]}</p><divclassName="gap-1 flex"><span>{key}</span></div></button>))}</div></div>);}
Enter fullscreen modeExit fullscreen mode

When we run this application, it will look something like this:


Benefits of Testing

For instance, suppose this application is shipped, and we can continue with our lives peacefully. But one day, there's a request to add a feature!

Users can write text but can't delete it!

We can say there's a new functionality added, “User can delete text.”

What can we do to implement this functionality?

Adding Delete Functionality to the FSM

Firstly, of course, we add the delete function. Here, I add a new event named “DELETE.PRESSED” that can be sent when the FSM is in the “Idle” state.

Delete functionality in FSM

This event will trigger an action named “onDeleteLastChar” that will delete the last character instr.

Do we immediately add this event to the UI? Certainly not!

Adding a New Test Case

After adding the delete functionality to the FSM, we need to write a test. Here's the test case I wrote to test this functionality:

describe("phoneKeypad",()=>{// ...it("pressing delete will remove the last char",async()=>{constfsm=interpret(phoneKeypadMachine.withConfig({})).start();fsm.send({type:"KEY.PRESSED",key:0});fsm.send({type:"KEY.PRESSED",key:1});fsm.send({type:"KEY.PRESSED",key:2});awaitwaitFor(500);expect(fsm.getSnapshot().context.str).toBe("1AD");fsm.send({type:"DELETE.PRESSED"});expect(fsm.getSnapshot().context.str).toBe("1A");fsm.send({type:"DELETE.PRESSED"});fsm.send({type:"DELETE.PRESSED"});expect(fsm.getSnapshot().context.str).toBe("");});});
Enter fullscreen modeExit fullscreen mode

To ensure this test is passed and the previous test cases are also passed, open the terminal and run the following command again:

yarntestphoneKey.fsm
Enter fullscreen modeExit fullscreen mode

If everything goes smoothly, your terminal should display the following message:

 PASS  src/features/phoneKeypad/phoneKey.fsm.spec.ts  phoneKeypad    ✓ should be able totypeusing the keys(1626 ms)    ✓ pressing different key will added the current key to string(507 ms)    ✓ pressing delete will remove the last char(514 ms)Test Suites: 1 passed, 1 totalTests:       3 passed, 3 totalSnapshots:   0 totalTime:        4.621 s
Enter fullscreen modeExit fullscreen mode

What I like about having tests is that I can ensure all the existing functionalities still work according to the written test cases. If there are indeed changes to the functionalities, then the test cases should also change and adapt. But if not, I can still use the same test cases!

Adding a Delete Button

Finally, we only need to add a button to send the “DELETE.PRESSED” event from the UI

// ...exportfunctionPhoneKeypad(){// ...return(<divclassName="h-screen w-screen flex flex-col justify-center items-center"><divclassName="grid grid-cols-3 gap-4">{/* ... */}<buttonclassName={["col-start-3 col-end-3","w-[90px] rounded-lg bg-gray-100 flex flex-col justify-center items-center py-4","hover:bg-gray-200 cursor-pointer active:bg-gray-300",].join("")}onClick={()=>fsm.send({type:"DELETE.PRESSED"})}>          DEL</button>{PHONE_KEYS.map((key,index)=>(// ...))}</div></div>);}
Enter fullscreen modeExit fullscreen mode

Our application now looks like this:


Conclusion

In this article, I have shared how I test the FSM I created with XState4 imperatively using Jest. By writing tests, we gain the confidence that at least the FSM we created works according to the given test cases.

Please remember, XState version 5 (the latest) has slightly different APIs, but the principles are more or less the same. Additionally, XState also provides a package for testing with a model-based testing approach. This package can also automatically create test cases based on the provided state machine definition!

Thank you for reading my article! Have a nice day!

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Learning entushiast.
  • Location
    Bandung, Indonesia
  • Education
    Telkom University
  • Work
    Software Engineer at Typedream
  • Joined

More fromIbé Dwi

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp