September 20, 2022

Backend Testing Best Practices

Backend Testing Best Practices

Running unit tests on your API's many endpoints has to be logical and repeatable, and the process of writing those tests should be as well. In this post, I will walk through the best practices that I follow for testing. I'll be within the context of a Feathers Typescript API, but most of these concepts should be universally applicable as I use a database of guitars as an example.

Setting Up Safely

Have you ever:

- Had your tests pointed at your production environment?

- Accidentally wiped your production database on a Friday at 5pm? 

- Frantically tried to restore the database with your laptop tethered to your phone from a Bed, Bath, & Beyond parking lot?

I have! And I don't recommend it.

Here's how you can protect yourself from that kind of misery, by setting up safeguards that prevent tests from running on anything but a test environment:

Building Out A Test Database

Phew! We should be safe now, so let's work on propping up our test data. We'll start by creating a class with migration, seeding, and clean-up functions that we can re-use across our test suite.

Now, let's plug those setup + teardown functions in to our setup file, comforted by the fact that none of them will run if the safety check fails.

Writing Tests

Our tests will cover the following methods that can be taken on guitars:

  • Create
  • Fetch - includes Feathers' find() (multiple records) and get() (single record)
  • Patch
  • Remove

For each test, the first thing that we want to check is that authentication is being enforced as expected for endpoints that create, read, update, and delete (C.R.U.D.) data. Other than a login/authenticate endpoint, we'll probably want every endpoint to require the user to be authenticated. For Feathers specifically, I'm using these auth helper functions for every method.

Here's a basic, boilerplate example with each method and the auth helpers being tested:

We have a foundation, now let's dig in to the meat of the tests.

I like to follow the pattern of Arrange - Act - Assert to lay out a standard operating procedure for each test

  • Arrange: Your setup. This usually involves initializing variables and gathering any data that you need
  • Act: Do the thing. If you're testing if deleting a guitars record works correctly, you're going to call the remove method of your guitars endpoint
  • Assert: Did the action produce the intended result? This is the core of the test, where we compare actual against expected values.

Here's a simple example of the pattern for creating a guitars record:

We'll be applying Arrange - Act - Assert to each C.R.U.D. method, and the pattern can require a bit more setup. Like in the case of updating an item, where we have to create guitar in order to test whether or not we can delete it:

Final Test File

When we piece our auth helpers, CRUD tests, and patterns together, we end up with a test file that covers each way that a user may interact with the API. This includes executing API methods on multiple records and using query params

This could also be interesting for you