Mocking API Endpoints in Playwright

Wade Green
Software Development

Intro

Developing an application with a split frontend and backend can have many benefits. However, it can add complexity and require more comprehensive testing strategies to ensure that it runs smoothly. When testing the frontend, it is important that your tests don’t send any actual requests to your backend servers, so that your tests can run independently and not touch any data in a live server.

One way to do that is to write a Mock API request handler that intercepts any requests sent from the frontend to the backend and responds with an appropriate predefined sample response. In this article, we will explore how to use Playwright to set up a Mock API request handler to help us to write our frontend tests. In order to give you a detailed description of our decision-making process, we will try to answer the following questions:

  • Why do I need frontend tests?
  • Why use Playwright?
  • Why do I need a Mock API Request Handler?
  • How did I build the Mock API Request Handler?

Why do I need frontend tests?

Testing is a great way to make sure that core features in your application are working as intended. Frontend tests are particularly useful in applications where the frontend and backend are separated, where certain bugs may not be apparent when testing the application manually. If a developer encounters a problem, it is not always immediately clear where the developer should be looking to solve the problem.

For example, imagine a developer starting up their backend server and frontend application to try and test a project. When she loads the home page, she sees that the table is rendering, but it has no data in the table. Clearly, there is an issue, and so the developer starts to imagine what could be causing the problem. Is the missing data a result of the frontend failing to send a valid request? Or did the server encounter an error, fail to complete the processing of the request, and then crash without sending a response? If the server did send a response, did the frontend fail to parse that response, and just decide to display nothing? Did my computer’s operating system install an update last night and accidentally erase my database? All of these are possible causes of missing data, and the developer may be forced to check on each of these possibilities before identifying the cause of the problem. Investigating these possibilities could take anywhere from a few minutes to a few days, and, if multiple issues arise, they can cause significant delays in application development.

Why use Playwright?

For our frontend, we decided to start with writing end-to-end tests. End-to-end tests are also a great way to start the process of frontend testing, as they are generally faster to write than unit tests and can cover large sections of code. End-to-end tests are also usually abstract enough that you don’t have to rewrite the tests every time you make minor changes to your code. There are many libraries out there that can assist in writing end-to-end tests, but for this project we chose to try using Playwright, a library that is focused around rapid development for end to end testing.

We chose Playwright for a variety of reasons. First and foremost, the library has bindings in multiple languages, which was helpful because our teams currently use a variety of languages for both frontend and backend solutions. Playwright also has native tooling to test across multiple browsers built in with no additional configuration required; by default, Playwright runs your tests against a Chromium-based rendering engine (the engine behind Google Chrome), a Webkit-based rendering engine (the engine behind Safari), and a Firefox-based rendering engine. Furthermore, the tests can be run cross-platform, including Mac/Windows/Linux and mobile devices, and they even have documentation around how to integrate the tests into Github Actions and CI/CD platforms. Playwright also includes a headed debugger so that you can visually see your tests in action, step by step. Finally, Playwright includes a Codegen tool which allows you to write code for your tests in any of their supported languages by simply clicking around in the browser. The codegen tool will write code that copies all of the actions that you perform so that you don’t have to spend time manually writing down actions that the user takes, and you can instead focus your energy on writing comprehensive tests to ensure that your features work as intended.

Another option we considered was Cypress, which also is designed to have simple bindings and is aimed at speeding up development of end-to-end testing. I personally had previous experience with writing tests in a more manual fashion using Selenium and BeautifulSoup, which was a powerful combination but somewhat tedious to develop tests in. After weighing our options, we decided to go with Playwright, Cypress would have also been a reasonable choice for writing end-to-end tests.

Why do I need a Mock API request handler?

For our app, we were trying to ensure that our end to end tests tested our frontend code without touching the backend server. We had a few reasons why we wanted to isolate the frontend from the backend during our tests.

First and foremost, we wanted to make sure that any failures or errors that come about in our tests were solely because of an issue in the frontend code. If we had written our tests in a way that depended on a running backend, then any issues or changes in the backend code could result in a failure in our frontend tests. Although we have backend tests that should surface any bugs, there could be unforeseen problems in the overall request lifecycle that only surface when the frontend receives a response. By ensuring that the frontend tests receive predefined responses, we can eliminate the possibility of backend errors causing failures in our frontend tests.

Apart from the above separation of concerns, there are a few other benefits to structuring our tests solely around the frontend. By isolating the frontend and not requiring a running backend, we can reduce the complexity of our tests, which reduces the time that our test suite takes as well as the computational power required. These time savings are not only beneficial to developers, as we spend less time waiting for tests to pass, but also result in time and cost savings in our CI/CD process by limiting the time spent checking deployments. Finally, because our frontend tests do not depend on a running backend, we can ignore any changes that are made to the backend code that do not affect the request/response cycle, thereby reducing the amount of time we need to spend updating frontend tests when making changes to the backend.

One important downside to mocking the handling of API requests is that, if you make any changes to the backend, you need to update your Mock API request handler accordingly. If there is any discrepancy between the response that your Mock API request handler provides and what your backend provides, it could result in unknown and unforeseen bugs even though your tests are passing. One way to solve this could be to have your predefined responses in your frontend tests be dependent on a fixture file that is generated during your backend testing process; when you update your backend tests, those new tests could update your response fixture accordingly, and then your predefined responses would always match your endpoints. This implementation is outside the scope of this article, but it is important to consider this possibility of introducing bugs by failing to update the predefined responses.

How did you solve the problem?

Once we identified why we wanted to set up the Mock API request handler, our next step was to implement one. Because Playwright is a very flexible framework, it allows you to write tests for a variety of application setups. Unfortunately, the fact that it is so flexible means that there is often more than one way to do things, and the documentation may not cover exactly what you are trying to accomplish. At first, it was a bit of a challenge to determine the exact method calls to use and how to structure our testing utilities in order to capture all requests and handle them properly. We tried a few options, including building out chains of switch statements and regex filtering to try and handle our requests, but eventually settled on an implementation that maps the queries directly with their responses. This approach made it easier to develop a modular system that can be easily adapted and expanded to cover as many endpoints and request types as we needed to ensure comprehensive coverage of our API endpoints.

Our goal was to ensure that any API calls that our frontend was making in our tests would be captured and handled by our Mock API request handler. After considering this goal, we broke down our approach into the following tasks:

  1. Determine what endpoints were accessed by the frontend
  2. Determine what requests were being sent to the backend
  3. Generate an example response that would come from the backend
  4. Write a request handler that would accept requests and return the corresponding response
  5. Ensure that the request handler ignores any uncaught requests
  6. Write tests

The first challenge was to determine which endpoints were being accessed by the frontend. For our app, we were using GraphQL for our requests, so all requests were being sent to the same endpoint: mysite.com:8000/graphql. If you are using a Rest API, you will likely have a variety of endpoints that you would need to mock; this can be accomplished by setting up a separate check for each endpoint that you want to cover.


TIP: If you’re not sure what url to use for the route, you can use “**” as a wildcard; for example, I used “**/graphql” to be sure I caught any requests to the graphql endpoint. You can then call route.request().url() to return the url that the request is being sent to, and use that in your future tests to be more specific about the requests you want to handle.

The next step was to determine how to pull the query out of our request. Our first set of tests dealt with making sure a user could properly log in and out of our app. We started by storing a const of our login query as loginQuery and an example response as loginResponse. The fastest way to accomplish this was to run the server locally, send a login request, and then check the web browser developer tools to see the raw request sent and response received and save those within our test. We did the same for our logout request and response.

Once we had our requests and responses, the next step was to check the incoming route object and respond with the appropriate request. The Route object has a .request() method that we can call to get the request object, and then we call the .postDataJSON() method on the request object and check that post data for a query. If we find the query, then we want to check that query against the queries that we intend to handle. We set up an object that would match each pre-programmed mutation request with the appropriate response, and then just check to see if the incoming route was listed as a key in our object. If the request was found, we use the fulfill method on the route to respond to the request with our pre-programmed sample API response.

If the request was not in our object, then we simply perform the default case, which is to do nothing. Initially, we had the default case call the route.continue() method; however, this was causing our tests to time out. Our frontend had code in place to automatically retry a query if it failed; since we had no active backend, any query that was not handled by our mock API handler would fail and be automatically retried. By doing nothing instead of allowing the request to continue, our frontend does not get caught in that endless retry loop.

From there, the rest of the process was to have the tests simulate clicking around the website in order to send the requests to the backend for our Mock API handler to intercept. The process of writing the code is made a lot faster by the Playwright Codegen tool, which allows you to click around like a user would and generate code based on your actions. If you’d like more information on code generation and test writing in Playwright, you can learn more in the documentation at playwright.dev .

Conclusion

Overall, this challenge was a great introduction to the Playwright library and has encouraged me to want to write more frontend tests for our applications. After facing some difficulty with getting into unit testing with more granular testing libraries, and the manual nature of some other end-to-end testing libraries, it was a refreshing change of pace to have such a clear and easy-to-use testing library like Playwright.

Although this challenge of building a Mock API request handler was daunting at first, the syntax of the library made it easy to break apart the request object, extract the relevant information, and build a meaningful response to return to the frontend. It did take some time and a few approaches to find the most efficient way to accomplish this task, but I am satisfied with this implementation. I feel like it will be easy to extend as our tests grow in size and complexity, as well as make it easier to maintain as our backend endpoints evolve over time. I look forward to learning more about Playwright and building out more robust testing solutions.

Continue reading