State Modeling in React with XState Model-Based Testing
Transcript from the "Model-Based Testing" Lesson
>> So for this final part, I wanna talk to you about, actually testing your application. Now interestingly enough when testing an application that might have these complex flows where you can add, delete a timer. Go back and forth between clock and timer, all of these different things. You could test it in a model based testing way using x date without your application having to use x date.
[00:00:29] So if we created this entire application with basically whatever we want to use, whether it's redox, WebEx, recoil, or any of these other, reacts libraries for managing States. It doesn't really matter we could create model based tests for them. So let me just go over what a model-based test is.
[00:00:54] In essence we have if you're familiar with the given when then style of testing you have given, which is when the app is in this state. And then you have the one part where it's like when this event happens in that state, then you're asserting that the app should be in this other state.
[00:01:17] So if you squint that sort of looks like part of a state machine. And indeed you could describe the behavior of your application as a state machine. So there isn't going to be an exercise for this, it's just something that he wants to explain to you. If we go to App.test.j s.
[00:01:40] We're we have another machine over here. This is called the testTimerAppMachine. And looking at the flow of this machine, we had our initial state of new timer. And this is not going to be the same machine that we use for the application. And the reason for this is because we want to create a machine that describes the user's point of view.
[00:02:04] And so you can imagine like going to a restaurant and ordering food. From your perspective, you tell the waiter what you want. And you see them go away and then maybe 20 minutes later you have your food. So you might think okay, those are the states you're ordering, you're waiting and then you're eating three states.
[00:02:26] But in reality, the implementation details of that might be something a lot more complex. The waiter has to go give the order to the kitchen. The kitchen has to make the order, and then the waiter has to serve other tables. A lot of things are happening in between that waiting and eating state, and that might not be visible to the user.
[00:02:48] So, yeah, we should be testing from the perspective of of user. And so that's what we're doing here. If we look at the testTimerAppMachine again in App.test.j s. We have an initial state of new timer for instance. So if I reload this and go to complete, that's what I see as a user right now this new timer state.
[00:03:13] And so I'm from here, I could do two things. I could change the value, which is just entering a number here, and then I could play. Now as a user, I know that I could only play, if I've changed something, so I add a guarded transition over here that says.
[00:03:34] If the value is greater than zero, only then can I go to the timer state and then in the timer state I could do a couple things. I could delete this timer, or I could add a new timer and adding a timer takes me back to this newTimer state.
[00:03:56] So there's this a library? Called x data test which allows you to create a testing model. Currently, it's called create model where you pass in that machine, and you specify what each one of those events mean. In this case, we're using react testing library, which is an excellent library in my opinion, better than enzyme for testing react applications.
[00:04:23] And so with react testing library, I highly recommend you read the documentation and look up how it works. Because there's a lot of libraries under the testing library umbrella for view, or for spelt, or even for just Raw DOM. And so, for example, to execute the change event.
[00:04:47] We're telling this testing model that executing it means we're finding this input that has a duration title. And if you could see here, but if you hover over this,you'll see the duration title show up there. And then we add 1,2,4 to the input. This is just a random number that I decided to put there.
[00:05:14] And similarly the play event means we're finding the add button. By getting this starts whatever timer button and then we're clicking it. Delete means similarly we're getting the delete button and clicking it and add we're getting the Add button and we're adding it. Now you'll notice here we describe the state machine.
[00:05:35] And we described each of the events over here. So where are the actual tests? Well, this model has this get simple path plans that describe all of the paths from the initial states to every other states. And it generates that for you. And that's the basic idea behind model based testing.
[00:06:02] So if we look back at our machine over here. We could see that from the idle states, there's a few paths that we could take. We could go from idle to running on the toggle event. We could also go from idle to running to paused or we could go from idle to running to paused to running again.
[00:06:27] Or we could go from idle to running to pause and then all the way to idle. So there's many ways to you to go about this machine or to traverse it. A simple path is a path that doesn't repeat states. So it's not the shortest path. But it is a path where, for example, we go from A to B to C to D, but never A to B to C to B to D.
[00:06:53] Because we are not repeating that these states. So with react, sorry with XD tests, you get those plans and each plan has a description which is generated for you. And each testing plan has a path which is generated for you. And that path is basically going from one state and traversing these transitions to another states.
[00:07:21] And, in each of the tests, we rendered the timer app and then we test that rendered timer app. There's a few articles on this by the way, I'm just giving you a brief overview of it. The way that we test each state is in the meta, test property.
[00:07:41] We could say when we're in the state, for example, when we're in the new timer state, we should see a newTimer or something with data test id =new timer. And this is going to throw an error by the way, if it doesn't find that test id so. So let's actually run these tests and show you what the output is.
[00:08:09] So if I run yarn test, It's going to be calculating all of those simple paths. And it's going to show a failure, cannot find module complete timerApp, that's completely unrelated. So Let's just see what that's called. It's called something else, it's called EPP, so we get EPP oops from app.
[00:08:38] Okay, so you could see here that the be tested didn't find that timerApp. So we're going to this is why testing is good. The errors are expected. All right. So when the test runs, it's going to generate in this case five simple paths. So for instance, it says it reaches state machines, new timers, no timers.
[00:09:06] So it's saying that it reached that child, no timer state, which is actually a really trivial test. That's basically testing. Does it end up or when you load the app? Is it on this screen? Is it on this no timer screen? And then there's this one. We're testing it reaches date no timers, with 124.
[00:09:29] So basically it's saying when we type in a value, in this case 124, it should still be on this no timers state, so nothing should change which Is exactly what happens. And then we have reached a state timer, which is saying that. When we click Change, or sorry when we do the change event, which is typing 124 in here, and we click play, we should be in the timer state.
[00:09:58] So that's one of the paths that's generated. And then there's reaches state new timer after deleted. And so that's if we click or if we change it, we play it, and then we delete it while it's running. It should be in this new timer state. Now, the reason that we have nested states over here is that.
[00:10:24] If we, let's say that we just go to new timer and we don't have this after deleted. So, go ahead and save that. You're gonna notice that it generates less tests and it doesn't even test that delete events. And that's because the model based tests are seeing that, we already been to the new timer state and I already asserted it so I don't want to visit it again.
[00:10:55] So we could tell it that this is actually a different state. This is the new timer state after a timer has been deleted. So we're sort of giving it some nuance and saying it shouldn't be the same new timer states and so on. So yeah, that's essentially how model based testing works.
[00:11:20] The more tests you add, the more paths it could possibly generate. There have been people who have generated maybe hundreds or thousands of tests from this. But the idea is that your tests are auto generated instead of manually specified. And if any behavior In your app changes, you change the machine, and the tests all get updated.
[00:11:45] So that's what model based testing is about.