In this talk, we will discuss some best practices for testing Vue 3 applications. We will explore how the Mock Service Worker and the Vue Testing Library can aid us in testing Vue 3 applications closer to a real-live user situation. Attendees will leave with a solid understanding of how to effectively test their Vue 3 applications to ensure reliability and maintainability.
Testing Vue 3 Applications with Mock Service Worker
Vue.js London 2023
Hi there, I want to tell you today about testing V3 applications with Mux ServiceWorker. I'm Lizzie, I'm a front-end architect at Storyblock. You can find me on Twitter, but most importantly, you can find all the examples that I made for this talk in this GitHub repository. And let's dive right into it. So this talk, it's based a little bit on Harry Potter, and we will figure out how we can use V3 together with Mux ServiceWorker to improve our code quality and to test in a better and more integrated way. So why is it important to test V3 applications? It's really crucial to perfecting your code, to making sure it's doing what it should. It can really help you to catch bugs before they go into production, because if you catch a bug in production, it's a lot more expensive. So the earlier you catch the bugs, the better. And it can really help to increase the confidence in your code and the code you're writing and also the code you're changing. So what is Mux ServiceWorker? Mux ServiceWorker is a really nice library that you can install into your application, and it allows you to simulate server responses in your tests. So you can create a mock api, and you can test how actually your application is handling the responses of the server and also handle edge cases of server responses, which might return errors. Why would you use that? It's really important in large applications that have a lot of api interactions. So many large applications like Storyblock, they have a lot of api interaction, and you want to make sure that all these different components, they're actually working as they should work with these different responses you can get from the api. It's also really nice if you run your unit tests suite in a continuous integration environment. For example, you run your unit tests in github actions, and inside of that continuous integration environment, you cannot call the api. So you need to mock the api and run the unit tests with extra mocked responses. And that's where using Mux ServiceWorker is really nice. All right, let's look at the example I built and how we can actually set it up. To start using Mux ServiceWorker, you can start using it in a vue 3 app. You need some kind of testing environment. So in this example, we'll be using Vydas, but it works with Jest just the same. And you need Mux ServiceWorker, which is the library that is in the core of this talk. So the example I built, it's showing my magical beasts. We have a headline, we have some cards with vue-astic, and we can click on those cards and then see some more details of the magical beast. And all the content here, it's loaded from an api. So I have a Storyblock space where I set up some content. I have a page that has the headline and the different magical beast. And then I can also see what's actually returned there. So through the api from Storyblock, I get the actual content that I can then use to show my content. The same for the more specific pages. I have a magical beast, and then I have some content here that shows all the details of that magical beast. All right, so let's start. The first thing for making a Mux ServiceWorker setup is that you actually need some kind of api. You need some kind of asynchronous way of interaction. So in my example, I have a component that fetches just a simple fetch to Storyblock's api, and that has all the responses. And then from that response, I build the actual page that we see. And the important part here is that now we need to mock the api. So our first step is to actually create a mock directory. In the root of our project, it could also be somewhere else, but I will do it in the root. And then we set up the Mux ServiceWorker. So what do I mean with a mock directory? If we look into the example, we have a folder there that's called Mux, and then we have two JSON files. So we have the beast JSON that's the start page, and then we have the Niffler JSON that's a mock of the actual more specific page. How can you get those JSONs? The simplest thing is to have your application and then check in the network what is actually being returned by the api. So if we reload that, my mock here is basically this request and this response we have here. So this story, I could just copy that and paste that here into my beast JSON file. So this one is just a copy of what I had on that page. So that's really the first part to have some mocks that can be used by the Mux ServiceWorker, but also by other unit tests that might not need the Mux ServiceWorker. All right. And then the next step is to actually set up Mux ServiceWorker and to set up some handlers. So for setting up the Mux ServiceWorker, we need to actually create a setup file. So in the white disk config, we can configure some setup files. So here we will provide the path to the file that is setting up our testing environment. Then here in test setup, I have this index.js file that has my global testing setup. So that's the setup that is used in all the tests that I'm writing. I also have some other stuff here, but the important part is here the Mux ServiceWorker setup file, where I connect Mux ServiceWorker with YDest. So before all, after all, and after each test, we're starting the server, we're closing the server, and then we're resetting the handles after each test. And then the actual server that we're creating, so that's the core part, we're importing from Mux ServiceWorker and we're providing it with some handlers. The handlers are the core part of setting up Mux ServiceWorker. And what the handlers do is that they connect a REST api, or you can also do a graphql api with a specific method. So you say, for example, I have a REST GET request that calls the endpoint api store block whatever. And when that happens, I want to return the status 200 and then please return a JSON of this mock that we just created. So we're returning a mocked response, and we can use that then in our unit tests. And we do the same thing for the more specific page of the magical beast. So here we're really connecting JSON example response with the endpoint, and this connection will help us write then the unit tests. So you do not need much more than that, setting up the server and setting up the handlers, and then we can already start flying and write our first unit test. So the next step here is really to write a unit test for this page. When writing a unit test, you have to think, well, what does it do? It shows a headline and it has three cards that have some content. So the view setup of it is pretty simple, but the complexity here comes a little bit with the asynchronicity. So we're testing an asynchronous component, and in vue, we can do that by wrapping it in a suspense component. So this is a fairly new component in vue 3 that can help us deal with asynchronous behavior. All right, let's look further into it. So we have that file in our vue that's asynchronous, like here, for example. We have some html, and the important part is that we have a fetch and we have to wait for the response of the fetch. If we look into that component, we can see here we're importing the component, and then we're wrapping it in an asynchronous component just so we can test it, because it is an asynchronous component. And then we can write the actual test. So in the actual test, what we do is, since it's asynchronous, we wait for the fetch to happen. How you can do that together with mock service worker is by creating a spy. So we're creating a spy on window fetch, and we will call that getSpy. You can do that also with Axios, so you could create a spy that works with Axios that does the same kind of spying. It really depends on which way you're using to talk to your api and get your responses. The next step is to actually mount that component. So we get this mount function from the viewTestUtils, and then comes the asynchronous part. We wait for this get, this fetch function to have been called one time, and then we use a helper that's called flushPromises. It's a helper for the viewTestUtils. It helps us make sure the promises are resolved. And then finally, we write the tests. So we get the wrapper for the test. We get the h1 element. We get the text of that, and we said, well, the text of my h1 element should be my magical beast. And then we find all the elements that have the VA card class, and we say, well, there should be three elements that have the VA card class, so that are three classes. So it makes sure there's exactly three cards in this unit test. And that's basically already our connected unit test with mock service worker. And it's pretty cool because we don't have to write this really crazy extensive mocks. But with this global test setup, when our endpoint is called here with the window fetch, it will automatically fetch this JSON we have here. And the component works as it would work also in the real environment. And then when we write the test, we can make sure that the setup of that component is actually like it should be. So let's look at the test we wrote here. One second, I just wrote a file. Here. So here, we have this before each, so it mounts the component before each test, so that we do not have to rewrite this in every test. And then we have the different test cases. The first one says, well, the heading should be shown. There should be three cards. Then we have also another test case where we say, well, the second card should always show like an if-not element that tests actually for that content we have there to make sure the content is shown in the way that we want to show it. All right. And yeah, that's already connected. And now comes the interesting part, because mock service worker, it can not only simulate these positive responses, but it can also simulate error responses or longer delays. And we can do that by just creating new handlers that actually return something else. So inside of our mocks, I created a file that's called error handlers. And error handlers looks really similar to the handlers that we had before. Again, it's using the rest method from mock service worker. We make a get request to the same endpoint. But instead of returning a context status 200, we're now returning a context status 403. And then the endpoint will return an error message with not allowed. And that basically would mock a behavior where you say a user is calling an endpoint that is not allowed to call that input. For example, a user is calling an admin endpoint, but the user might not be an admin. To show you what I mean in the browser, I can show you that here. So here, if we look at the network again, let's reload that. And then we can block this URL. And now if we reload, it cannot load because this URL is blocked. The browser is not allowing us to. And then we see this error here. So I adapted my component to be actually able to handle this kind of error response. So inside of my home view, it's gotten a bit more complicated where I check the response. I check the response status. And if it's not a 200, I'm throwing an error. And this error is then shown inside of my page. And now I can write the test for it. So to test this behavior with errors, what we need to do is we need to import our server that we're using that is active. And we need to import our error handlers that have the actual error handling responses. And then I can write a new test suit for the failing requests. And I can say, please use the server with the error handlers instead of the other regular handlers to see how my component behaves with a failing api response. Then I wait for that to be loaded and finished. And then I write the test case where I say my html should contain this error response that is returned from the error handler. So the text that the api is returning should be shown in my html. And if we look at these error handlers, this is the message that is returned here. And then you're testing the same component, the same asynchronous component, with actual failing api responses, which makes it even more clear and make sure that we're also covering cases where the api might be failing. So this is really cool. And this is something that's rather hard to do when you have to write it yourself and you don't have the mock service worker. So you have to do a lot more without the mock service worker to get this kind of testing behavior. So it's really, really powerful. We can also test other edge cases. So APIs have unusual behavior and you can test test cases, for example, where the api is delayed for two seconds, or you can test cases where you have some custom error message or some custom response because some custom case happened, like a user calling an endpoint that he should not be calling. Of course, mock service worker is not the only way to test. You can use these mocks that we created also in unit tests that are not using the mock service worker. So for components that are not maybe calling an api, they're just regular components that are not even asynchronous. For the smaller units, I can really recommend using the testing library. It's a really nice wrapper around the view test utils that can test even better for accessibility. It can test if the right elements are on the screen. It can test for user interaction. So it's really good in making these click behaviors or like input field behaviors to make sure your components are behaving like they should for user interaction. I will show you a quick case of our example. So let's disable the blocking here. If we go to our element here, it's a simple component that receives a prop. We now want to test those components and maybe test this click behavior that we have here on the buttons or that we have a button. I will show you the actual test. So this is our component. So it is a simple view component with a lot of html. And it basically only receives a prop called block. That's very common in applications that use Storyblock. And then everything that's shown here is based on top of this block prop it's receiving. And then here we can go into the test for this component. And you see here we're importing the render screen and fire event functions from the testing library view. So that's kind of the core functions you would need when you test with this library. I'm also importing the config because I wanted it to receive some of the global configuration I already had configured. And then I import the component. Here I don't need this async wrapper because it's not an asynchronous component that's waiting for some api response. It's just a simple component. And I'm also importing this mock that I created in the early beginning where I just copied the response I got from the server. I then create the actual prop that is passed to the component. So what's passed to the component is inside of that whole mock. So I get the specific part of the mock that I need to pass it to the component. Then I render the actual component. I pass the content, the mock, to that component. And then I can have the different test cases. So here I'm testing that the name of the magical piece, the description, and the image are there. I can also test and that's what's cool about the mock. So I can test the image and then I can also test the text. So what's really cool about this library is that I can test by all text. I can test by placeholder. And this can be really useful for making your application more accessible because we do not only test for specific tests but for actual html attributes. And then finally I can also test some interaction. So here I'm saying I want to click on that button. I want to be sure that this component emitted this specific event. So this basically tests this behavior that this component is emitting an event when we click on it. And now if someone would go into this component and remove this button, then our unit test would fail and we would see, well, something went wrong because it's not fulfilling this button behavior anymore. The same thing would happen if this person would change this button to a div. The unit test would be failing because we've been really specific in saying, well, there should be a button with that name that should emit exactly this event. And this helps us make sure that our application is not breaking. And as you see here, it's not using the mock service worker. It's not asynchronous, but it's using the same mocks that we're also using together with the mock service worker. And that's really helpful because we have one place where we define the interaction point between the api and the application. So if all the tests are using the same kind of mocks and if my api is changing, I also have to change the mocks, but I can make sure that my components and my components when I'm testing them are actually working with all the things that are coming from my api. All right. That's basically it. I have some more resources. So mock service worker has a really nice documentation. The testing library is really great for testing simple components and testing interactions. vue.js has also a lot of great guides on how to work with asynchronous behavior, how to work with suspense, and also how to test asynchronous behavior because it's not actually that easy. And yeah, you can also use Storyblock to create your APIs. And yeah, that was really fun to actually do and learn and set up. And I hope you use this repository to try it out in your own vue 3 application. Thank you very much for listening.