Please note that @testing-library/user-event version 14 has made all APIs async and the workshop material has been updated to use the latest version of that package. So you'll need to make all tests async and use await when using that package.

Transcript

Kent C. Dodds: 0:00 We should probably take this window.fetch mark and move it somewhere more and more general, so it can be used for all of our integration tests.

0:07 Pretty much, every time we do an integration test, we're going to be hitting the bootstrap endpoint. Anytime we load up a book screen, we're going to be hitting this endpoint. We've got a lot of work to do. We've got tons of endpoints that we're hitting. We need to implement all of those.

0:21 I'm not super happy with the way that we've implemented this, anyway, because it's not complete. It's not giving me confidence that we're making window.fetch calls correctly.

0:29 For example, what if we made a PUT request to the bootstrap endpoint rather than a GET request. Maybe, we added that or we made a typo or something. Then, there's no way currently to differentiate between those two.

0:42 We'd have to add additional logic here to verify the config. There is plenty more where that came from. It actually gets pretty messy. I'm really happy that MSW exists. We've actually already configured it for a previous test with our API client.

0:57 Here's how that works. It simply grabs setup server. We grab some server handlers that we've configured already. We set up our server. Then we set up our server to start listening before all of our tests. After they're all done, then we close the server so we're no longer listening.

1:11 Then between each test, we get rid of all of the runtime handlers that were added for the specific test. We just reset it to the server handlers that we initialized it with in the first place. Now the server handlers already exist, because we've been using them this whole time as we interact with our application.

1:29 We use them for development. This makes our development time really fast if the backend hasn't implemented an endpoint yet, or if we want to work with this offline, or maybe the backend server is down or something. We still want to get some work done.

1:43 Having MSW set up for us makes the development experience really nice. It also allows us to have some really cool DevTools here where we can control exactly what our backend is doing, because it's all in mock backend during development.

1:57 It's really easy to switch this off so we can verify that this all works in production. In our end-to-end tests, it will probably want to hit production for those. For our integration tests and for our regular development workflow, this is really nice.

2:10 Because we've already made all of this stuff up for our development workflow, we can reuse all of the same stuff for our tests. Now with the way our tests currently work, we create this user and then we send that user in our mock Fetch API.

2:26 If we're going to be switching over to MSW, we need to have some mechanism for MSW to know what to do when we make this bootstrap request and to know where to get this user from. The way our MSW server handlers do this is they interact with these fake databases that we have for our development.

2:44 We can take that these exact same modules interact with those modules, and then MSW will be able to retrieve our user, and our books, and our list items straight from those databases.

2:54 Let's start out by getting rid of this window.fetch mock. Because we have things already setup to listen for the requests that we're going to make, if we save this, this should actually hit our server handlers for MSW, which it looks like it does.

3:09 We're off to the races already. Now, we just need to make sure we put a real legit token for a real legit user into localStorage to fool our auth provider that we are logged in. We can use that same token when making our requests.

3:24 Let's stick this user in the database. We'll say await User.db('create-user'). Then we need to get a token for this user. We're going to say, our authUser = await users.db, authenticate with that user. Then the authUser has a token. We can stick that token in here.

3:45 Then for the book, if we're going to be loading up this book page, that book needs to be in the database as well, so MSW can retrieve its data. We'll say await books.db('createBuildBook'). Now let's save that and see where that lands us here. Alright, great. So far, so good.

4:03 The next problem that we come up against is adding MSW to the mix makes things take just a tiny little bit more. Maybe another tick of the event loop or something. It's not really anything to be concerned about, because it is just a fraction of a millisecond longer.

4:19 Because of that, we actually get rid of the loading spinner and then we have our book loading indicators here that we need to handle as well. I'm going to enhance this to actually await for a bunch of elements to be removed. We want all the elements that are labeled loading, and all elements with the text content that matches loading to be removed before we continue with our test.

4:41 I'm going to turn this into an array, we'll spread the results of this query. Instead of getBy, we'll use queryBy, and we'll use, actually, queryAllByLabelText and we'll do queryAllByText. Everything that's labeled loading, or has the text content loading should be gone from the page before we continue to make our assertions.

5:04 Let's save that and hopefully this passes, which it does. Awesome. We're in a really good place here now, because we can make as many of these tests as we want and we've already got all the endpoints or backend mocked out. We don't have to worry about it.

5:19 Now, this does take a fair amount of time to build out and there are ways to generate these handlers depending on the standards that your backend is implemented in. In general, it's absolutely worth the effort, because it makes writing integration tests easy, and it makes development time a lot better as well.

5:39 One of the thing, that I want to do here is because we're creating things in the database, we should probably clear that between every one of our tests to make sure that every test starts with a clean slate.

5:50 We're going to add await users db.reset, and we'll do books db.reset, as well, and listItems db.reset. Then we've got all of these, they're not dependent on each other. I'm actually going to say await Promise.all and we'll just have all of these go off at the exact same time. There we go. Now, we're cleaning up after every one of our tests as well.

6:15 Let's review what we did here. We had a window.fetch mock right here that we just wanted to get rid of, because it wasn't giving us enough confidence. We swapped it out for the test server that we already had configured for a previous test that we setup.

6:28 That was configured to have all of these server handlers that were already written, so we can handle every request that our application makes. These server handlers interact with a database and for us to get our test setup before we actually render our app, we interact with that database ourselves as well.

6:45 Then we also want to make sure that we're in a good non-loading state before we continue with the rest of our tests. We enhanced our wait for element to be removed to make sure we're no longer in the loading state, and then we also cleaned up after each one of our tests.