How to handle uploading and parsing files in your frontend app tests
Over the last month, we ran into the challenge of testing a non-trivial user flow. In our Ember app, users can upload a file to our servers for creating transfers in bulk. In this particular flow, the server reads, parses and processes the data before sending a list of transfers in response. We can see how testing this flow could be complex.
If we wanted to test this kind of scenario and mock the endpoints to match this exact same process, we’d have to wire up a lot of code.
Traditionally, we’d set up a fake server, for example on top of a library like Pretender. We’d then mock our endpoint in order to return fixed data to match our server response. This approach can work if the app is small enough and the data structure is simple.
However, when a data structure gets complex, and there are multiple relationships between resources, things can get tricky. Also, in the context of an app that’s scaling at a rapid pace, this can be hard to maintain and scale.
This is where a library like Mirage JS is very useful. We’re going to talk about how we use Mirage in our Ember app at Qonto and how we solve the aforementioned scenario in a few simple steps.
What is Mirage?
At Qonto, we use it to mock the API endpoints we need to use in our tests. In Mirage, we call them route handlers. The same as we do on the real backend we also test these to make sure we match the expected request and response data.
Let’s have a look at a scenario we recently worked on.
Creating bulk transfers
One feature we have in the Qonto app is the ability to create bulk transfers. The user can do this by uploading a CSV file that would look like this:
In turn, the backend parses the uploaded file and creates the response below:
Different options for mocking in Mirage
One way to mock responses in Mirage is to use Fixtures. Fixtures are basically fixed data we use to mock our scenarios. In our case, we’d need to mock the server response after uploading a file.
Here’s what this would look like:
Using fixtures makes sense in some cases, but they are not maintainable or scalable. In our case, the uploaded file and the response can easily get out of sync at some point without us noticing it.
The alternative to using fixtures is to use Factories. These help us organise data creation logic and match the real API in terms of how resources are created. In our case, we have several related resources required by the Mirage route handlers, which makes it very hard to use fixtures. Here’s a simple example using Factories:
In the example above, we hardcode the transfer data. Next, we’ll explore how we can extract this data from a CSV file.
Mocking the file upload in the Mirage test
The problem with libraries like Mirage is that we’re not testing the real API. There is a high risk of introducing differences between the mocked API and the real one, leading to inconsistencies between the application and the tests. The solution to this problem is to test the mocked API and make sure it matches what we expect from the real API.
At Qonto, we’re testing all our Mirage route handlers to make sure they match what we expect from the real API. We’re going to employ a TDD approach to writing our route handler in this specific case, so we’ll start out by writing our test.
In our app, when uploading a file, we create a FormData object holding the file data and send it to the server. We need to mock the same in our Mirage route handler test.
Creating the Mirage route handler
The route handlers in Mirage are essentially the endpoints we need to mock. In these endpoints, we receive a request, process it, and return a response that matches the real API response.
We need to create a route handler for the
/multi_transfers endpoint to get the test working. We won't process or return anything in the response yet:
Reading and parsing the file
For reading the file contents, we’ll use the FileReader API. The FileReader methods work asynchronously but don’t return a Promise. To make things easier, we need to wrap its result in a Promise.
This way, we can
await the result in our route handler.
Next, we’ll write a function to parse the file and create the data structure we need to use later:
Putting everything together
Using the functions we wrote above, we can now complete our route handler and make it pass the test:
Wrapping it up
Testing file uploads and mocking them in tests is not a trivial task. There are libraries like Mirage that we can leverage to get the job done with less time and effort.
Mocking the file upload for multi transfers in tests allows us now to use the same data creation logic as we do for any other resources when testing our app. This results in reducing our cognitive load when writing our tests, since everything else is abstracted away. Also, we can use the same file processing logic on any other similar endpoints. As a result, this improves developer productivity and has a positive impact on the overall quality of our products at Qonto.