course illustration

Test Node.js Backends

lessons icon40 video lessonsduration icon2h 21m of learning material

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.

You'll learn:

  • Testing Pure Functions
  • Testing Middleware
  • Testing Controllers
  • Testing API routes
  • Mocking third party dependencies
  • Testing authenticated code

Lessons

1. Intro to Test Node.js Backends

duration icon 4m

2. Test Pure Functions Overview

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.

duration icon 1m

3. Write Unit Tests for a Simple Pure Function

Let’s write the basic tests for our simple isPasswordAllowed function to ensure that both valid and invalid passwords receive the correct return value.

duration icon 2m

4. Improve Error Messages by Generating Test Titles

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.

duration icon 4m

5. Use jest-in-case to Reduce Duplication and Improve Test Titles

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.

duration icon 4m

6. Create a Casify Function to Generate Cases for jest-in-case

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!

duration icon 4m

7. Test Node Middleware Overview

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.

duration icon 1m

8. Write a Unit Test for Handling an UnauthorizedError

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.

duration icon 6m

9. Write a Unit Test for Handling headersSent in an Error Middleware

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.

duration icon 2m

10. Write a Unit Test for Status 500 Error Middleware Fallback

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.

duration icon 2m

11. Improve Test Maintainability using the Test Object Factory Pattern

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.

duration icon 2m

12. Use a Test Object Factory utils/generate

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.

duration icon 3m

13. Test Node Controllers Overview

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.

duration icon 2m

14. Write a Unit Test for a Controller by Mocking the Database

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.

duration icon 6m

15. Simplify Assertions on Error Messages with toMatchInlineSnapshot

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.

duration icon 5m

16. Unit Test Business Logic of a Controller’s Middleware

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.

duration icon 5m

17. Test Controller 404 Edge Case where Resource is Not Found

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.

duration icon 3m

18. Test Controller Unauthorized Case

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.

duration icon 3m

19. Test getListItems for Getting Multiple Mock Objects

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.

duration icon 5m

20. Test the Happy Path of a Creation Flow

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.

duration icon 6m

21. Testing an Error Case for createListItem

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.

duration icon 3m

22. Testing the Happy Path for updateListItem

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.

duration icon 4m

23. Testing Resource Deletion

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.

duration icon 2m

24. Test Authentication API Routes Overview

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.

duration icon 4m

25. Start a Node Server and Fire a Request to an HTTP API Endpoint

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.

duration icon 4m

26. Make Assertions on HTTP API Responses for Registration

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.

duration icon 1m

27. Test the Login Endpoint for a Node Server

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.

duration icon 1m

28. Test the User’s Resource Endpoint

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.

duration icon 1m

29. Create a Pre-configured axios Client for the baseURL

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.

duration icon 1m

30. Improve Error Messages with an axios Interceptor

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.

duration icon 6m

31. Ensure a Unique Server Port

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.

duration icon 5m

32. Use Snapshots to Assert on Server Error Responses

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.

duration icon 3m

33. Interact Directly with the Database

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.

duration icon 1m

34. Test all the Edge Cases

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.

duration icon 3m

35. Test CRUD API Routes Overview

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).

duration icon 1m

36. Write an Integration Test for a Resource Create Endpoint

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.

duration icon 3m

37. Write an Integration Test for a Resource Read Endpoint

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.

duration icon 1m

38. Integration Test a Resource Update Endpoint

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.

duration icon 1m

39. Integration Test a Resource Delete Endpoint

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.

duration icon 4m

40. Snapshot the Error Message with Dynamic Data

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.

duration icon 1m