Backends hold so much of our application's business logic that is often used to support multiple clients (web, mobile, and other native platforms). This logic is critical to get right and deploying a breaking change to this can be devastating to your company's goals (not to mention the bottom-line). Increasing your "deployment confidence" is crucial and automated testing is the best way to do that.
As Node.js continues to grow in usage around the world, learning how to test this mission-critical code in a way that increases developer velocity as well as confidence becomes increasingly important. In this workshop we use an Express.js example and focus on the patterns and practices that you need to learn so you can apply what you learn to test your code written in any Node.js web framework.
Of all things you can test, the easiest thing you can test is a pure function because they require no setup or teardown. In this exercise, we’ll explore testing a pure function called isPasswordAllowed.
Let’s write the basic tests for our simple isPasswordAllowed function to ensure that both valid and invalid passwords receive the correct return value.
One of the most crucial things you can do when writing tests is ensuring that the error message explains the problem as clearly as possible so it can be addressed quickly. Let’s improve our test by generating test titles so error messages are more descriptive.
Jest has a test-generation feature built-in called test.each which is great, but I don’t particularly like it’s API. Instead, we’re going to use an open source project called jest-in-case which gives us a really nice API for generated tests and improved error messages. Let’s try that library out for our isPasswordAllowed tests here.
Even with jest-in-case there can be a little boilerplate and you can easily side-step that by creating a simple function that allows you to write test cases that are more suited for your use case. Let’s give that a try!
There are various types of middleware and for many of them you write tests for them in the same way, so let’s get a look at one of these and try our hand at writing some tests for it.
We have an error middleware in our app for handling uncaught errors throughout our application. Let’s write our first test case for handling an UnauthorizedError which our authentication abstraction throws when someone attempts to access data they shouldn’t.
Our next test will be really similar to the first. This time we want to handle the case where a response has already been sent which you know if the res.headersSent property is set. So we’ll add that property to our fake response object and verify the middleware behaves correctly.
Our final case is the fallback scenario where we don’t know exactly why things are failing. In this situation we’ll just give all the information that we can and hopefully the developers can figure out what that is. Let’s write a test case for that situation.
We’ve got a little bit of duplication between these tests and in a more complex middleware we’ll have a lot of duplication. The duplication itself isn’t necessarily problematic. The problem comes in the form of readability of our tests. When there’s a great amount of duplication, it makes it harder to identify the parts of the test that distinguishes it from other tests. In this lesson we’ll apply the object factory pattern to reduce duplication and make it easier to maintain our tests.
We’re going to need the same utilities for our object factories in several places in our tests, so let’s use some pre-built utilities instead of the ones we wrote for this file.
Controllers are a collection of middleware that applies business logic specific to your domain. Typically these are tested like any other middleware, but often they require mocking the database for unit tests.
Let’s write our first controller middleware unit test by mocking out a database call and verifying that the database is interacted with correctly and the response is sent correctly.
Let’s add a test for an edge case that responds with an error message. In this lesson we’ll talk about the value of using the toMatchInlineSnapshot assertion for error messages.
Not all controller middleware send responses. In this unit test, we’ll verify that a controller’s middleware interacts with the request, response, next function, and database correctly.
Sometimes people try to access data that’s not there and it would be great if our server doesn’t fall over when they do, so we have some code to handle that case. Let’s get that tested and use toMatchInlineSnapshot for asserting on the error message.
If the user tries to access a resource they shouldn’t have access to, we need to make sure they can’t get that resource, and we’ll want a test to make sure that functionality doesn’t break because this is a data security concern. In this one we handle dynamic data in a snapshot by making a consistent ID for the user and list item.
We have another controller function we want to test that will retrieve all of the user’s list items. For this one we’ll need to mock out a few new APIs and build multiple books and list items. Then we’ll have to do a few things to ensure that the APIs were called correctly.
We want to test the createListItem controller function so we can ensure that users can create new list items. In this one we need to mock out several database requests. We also see how great Jest error output is which helps us identify a few mistakes that we made really easily.
Even though your edge cases don’t run as often as your happy path cases, they are important to test to make sure you handle those correctly. In this one we’ll verify that if a user attempts to create a new list item for a book even though they already have a list item for that book.
For this one, we’ll need to have an existing listItem and we’ll create some new notes for it. Then we’ll have the original list item and the updated list item and ensure that our API is being called with the right version of the list item.
This one’s a bit unique because it’s pretty simple. All we need to do here is verify that the remove method of our was called properly, but we don’t need to bother mocking out what it returns because our code doesn’t use that return value. We also want to verify that the res.json was called properly, so we’ll do that as well.
Let’s start doing some server integration tests. We’ll get our server started and database going so we can hit the endpoints in the same way clients will be using our endpoints. Let’s explore how our app is started so we can start it in our test environment and I’ll show you around the codebase so you can navigate around while you test things out.
Let’s get our test ready to roll by getting our API server and database started and in a clean state. Because our tests and server both run in node, we can do this all using the utilities that Jest exposes for us like beforeAll and afterAll. Then we’ll get a test started and fire a request to the API to make sure we can communicate with the server.
When you’re interacting with the API response of a server, some of the data you receive is information that you cannot assert on directly. In this lesson we’ll talk about how to use Jest’s asymmetric matchers to assert on the type of data your server is sending.
Let’s make another request in this same test to the login endpoint with the same post data as we used for registration. Then we can verify that the information we retrieved from the registration request is the same as the information we retrieved from the login request.
Our server has an API for authenticated clients to request information for the currently logged in user. Let’s make an authenticated request using the user token we retrieved from the login request to get confidence that the token generated will get the data for the user.
We’ve got a bit of duplication in our test for each request and it’d be nice to remove that, so we’ll create a pre-configured axios client to reduce that duplication.
If our request fails, the error message is really useless. The server sends us back a much more useful error message, but by default Jest wont display that error properly and we can’t identify the line of code that made the failing request. Let’s use a custom axios interceptor which will improve those error messages considerably by displaying the response that the server returned.
If you want to run your tests in parallel and each of your tests starts up the server on its own, then you could run into a problem where two servers try to bind to the same port. To avoid this, we can use the JEST_WORKER_ID environment variable so we can ensure that the port our server starts on is unique. Let’s try that out.
Let’s test the case where we try to register two users of the same username. We’ll first register a user, and then we’ll attempt to register a second user with the same username and assert on the error message. We’ll use toMatchInlineSnapshot on the error that’s returned and Jest will serialize that like a champ.
Often in integration tests you’ll have situations where you are over testing. Sometimes that’s acceptable, but you can speed up your tests by getting your application into a testable state a little quicker. In this lesson we’ll skip hitting the API for our setup and instead interact directly with the database.
Integration tests are typically best for common cases and you can cover other cases using lower level tests, but I thought it’d be interesting to show you how to cover a few other edge cases as well in this lesson.
Things change a little bit when we want to hit an endpoint that requires the user to be authenticated. For this exercise you’ll need to use an authenticated test with a test user that’s already in the database. Then you can use that test user for each of the operations you do on the list item resource (Create, React, Update, Delete).
Let’s get a test user and authenticated axios client, and then we’ll need to generate a book and insert that into the database directly so we can create a list item for our test user for that book. Then we can verify that the response data is correct.
Now that we’ve created a list item, let’s try to read that list item. This will give us confidence that the list item we created was actually saved to the database and can now be retrieved.
For the update, we’ll need to create new notes for our list item and send those updates to the updates endpoint. Then we can verify that the response we get back is the same as the original list item except with the updated notes.
For our delete endpoint, we don’t need to do too much beyond hitting the endpoint, but it could be problematic if we just hit the endpoint and verify the response. In this lesson we’ll find out why that is and how we can use our test to make sure that the item was actually deleted from the database.
Typically, it’s nicer to use toMatchInlineSnapshot for error messages, but sometimes the contents of the error message are generated. But I’ve got a little trick for solving this problem that you’ll probably find useful.