Get Your Node.js web app to 80% Code Coverage in 30 mintues

Get Your Node.js web app to 80% Code Coverage in 30 mintues

We know that testing is an essential step in the development process, but it can also be a time-consuming and tedious task. Common problems with API testing include difficulty in setting up test environments, and maintaining test cases when the API changes.

But what if we told you that you can automate this process and get

hundreds of tests in just 30 minutes

?

That's where Pythagora comes in, an autonomous testing tool that can help you automate your API testing process. In this blog post, you will learn how to set up automated API testing using Pythagora, and how it can help you save time and resources while ensuring that your application's API functions as intended. We will cover everything from the basics of API testing to advanced techniques for testing complex APIs. So, whether you are a beginner or an experienced developer, this blog post will have something for you. Let's get started!

API testing

API testing in Node.js is an essential step in the development process to ensure that your application's API functions as intended. It is a way to verify that the API is functioning correctly and returning the expected results. It can also help identify any errors or bugs before the application is deployed to production. One of the major advantages of API testing is that it allows for faster and more efficient testing of the application as it does not require a user interface

(for more details on API testing vs other types, take a look at this great post by <IME AUTORA>)

. Additionally, it allows for more comprehensive testing as it can be integrated with other tools such as load testing and security testing.

However, one downside of API testing is that it can be time-consuming and requires specialized knowledge. (napisati bolji point)

To implement automated API testing in Node.js, one can use a testing framework such as Jest or Mocha. These frameworks provide a set of tools and functions to make it easy to write and run automated tests. Additionally, one can use a library such as Supertest or Chai HTTP to simplify the process of making HTTP requests and asserting on the response.

Basic automated API test

Let's take a look at an example of how to implement API testing using the Jest framework and the Supertest library. First, you will need to install both Jest and Supertest. You can do this by running the following command in your terminal:

npm install --save-dev jest supertest

Next, you will need to create a new file in your project where you will write your tests. This file should be named something like api.test.js. In this file, you will need to import the modules that you will be using for testing. For example:

const request = require('supertest');
const app = require('../app');

The first line imports the Supertest module, and the second line imports the Express app that you will be testing.

Once you have imported the necessary modules, you can start writing your tests. A basic test might look something like this:

test('GET /users', async () => {
  const response = await request(app).get('/users');
  expect(response.statusCode).toBe(200);
});

In this example, the test makes a GET request to the /users endpoint of the Express app, and then it uses Jest's expect function to check that the response has a status code of 200. If the status code is not 200, the test will fail.

You can write more complex tests by chaining multiple .expect calls or by using other assertion libraries such as chai to assert on the response body.

To run your tests, you can use the command npm run test or jest on your terminal.

Advanced tests

Now, imagine writing these tests for let's say, 30 endpoints and check, not only the status code but also different parameters in the response body, redirect urls, etc. You would have to write a lot of boiler plate code for that.

However, previous example was a simple one

. Imagine you want to test an endpoint that returns products that a user has viewed before but hasn't purchased.

When testing that kind of an endpoint it's important to first set up the necessary data for the test. This includes having a user in the database along with associated products that user has viewed but hasn't purchased.

You want these tests to run from any machine (local machines from other developers or staging/testing environments), you need to make sure that the data we want to test is actually in the database. Since we don't know what is the state of the database, usually, the best practice is to set up database conditions before the test is run (using beforeEach function) and clear the database state after each test (using afterEach function).

For example:

let userId, productIds;

beforeEach(async () => {
  const products = await Product.create([
    { name: 'Product 1' },
    { name: 'Product 2' }
  ]);

  productIds = products.map(p => p._id);

  const user = await User.create({
    name: 'Bruce Wayne',
    email: 'bruce@wayneenterprises.com',
    viewedProducts: productIds
  });

  userId = user._id;
});

In this example, we first create two new products using the Product.create method. Next, we create a new user with name "Bruce Wayne", email "bruce@wayneenterprises.com" and we associate the created products to this user by adding product ids in the viewedProducts parameter.

Notice how we need to declare userId and productIds variables outside the beforeEach function. This is because we want to use the product ids and the user id in the actual test.

Ok, once the setup is done, we can then proceed to test the endpoint that returns the list of products.

test('GET /purchased-products', async () => {
  const response = await request(app)
    .get(`/viewed-products/${userId}`);
  
  expect(response.statusCode).toBe(200);
  expect(response.body).toEqual([
    { name: 'Product 1' },
    { name: 'Product 2' }
  ]);
});

In this example, the test makes a GET request to the /purchased-products/:userId endpoint with the userId that we created in the setup method. And it uses Jest's expect function to check that the response has a status code of 200 and that the response body contains the products we created in the setup method.

Now, imagine you have different types of users and products. For example, is a user purchases a products multiple times, they get it with a discount.

For each of these different cases, you will need to create a separate setup and a separate test.

Test that updates the database

Another very common test scenario is checking if database update happened. For example, once a user purchases a product, we want to modify its document in the database to reflect that. To make sure that the user collection was updated, we need to query the database and inside the test, compare if the database reflects the wanted change. Here is an example of such test.

const request = require("supertest");
const app = require("../app");
const User = require("../models/user");
const mongoose = require("mongoose");

describe("POST /purchase", () => {
  let user;
  beforeEach(async () => {
    await mongoose.connection.dropDatabase();
    const viewedProductIds = ["productId1", "productId2"];
    user = new User({
      name: "testuser",
      viewedProducts: viewedProductIds,
    });
    await user.save();
  });

  test("should purchase a product and update the user", async () => {
    const productId = "productId2";
    const response = await request(app)
      .post("/purchase")
      .send({ productId });

    expect(response.statusCode).toBe(200);
    expect(response.body).toEqual({ message: "Product purchased successfully" });

    const updatedUser = await User.findOne({ name: "testuser" });
    expect(updatedUser.purchasedProducts).toContain(productId);
  });
});

In this test, we are making a POST request to the "/purchase" endpoint with a productId in the request body. We are then checking the response status code to be 200 and the response body to match an expected message.

After that, we are querying the database to find the user object and checking if the purchasedProducts array of the user contains the productId that we sent in the request.

Phew, these tests are expanding more and more!

Remember those 30 endpoints we mentioned? To cover your Node.js codebase with tests as much as possible, you will need to code a test for each of the different cases for each of the endpoints. This can easily scale to hundreds or even thousands of different tests.

Also, keep in mind that a codebase is everchanging construct. That means that as the codebase changes, all these tests need to updated and maintained as well so they don't return false results.

Man, as I'm writing this, I get overwhelmed by such a daunting task of writing all those tests from scratch and maintaining them.

Authonomous testing

Because of all these reasons, we decided to build a solution that could do all of this by itself so we created Pythagora.

Pythagora is an autonomous testing tool that creates automated API tests for your Node.js app by itself.

It works by recording all requests to endpoints of your app, along with Mongo/Redis queries and replays the requests when you run the test command. During the test execution, Pythagora mocks all responses from Redis and 3rd party APIs, and simulates the database conditions from the time when the API requests were captured. This way you can run the tests from any environment regardless of the DB conditions.

When you run a test, Pythagora restores the DB state before the request is made so it looks as if you’re running the test on the original machine on which it was captured. If the request updates the database, Pythagora checks the database after the API returns the response to see if it was updated correctly.

Plus, it tracks lines of code that were triggered during API requests, so while you’re clicking around the app, you know how much of your code is covered by the tests.

The best part about this is that getting this to work is super simple! Here's how you do it:

First, you integrate Pythagora

  • npm install pytagora
  • Add Express to Pytagora - Eg. if you’re initializing Express with let app = express(); , right after that add the following lines:
  • if (global.Pytagora) global.Pytagora.setApp(app);

That's basically it when it comes to the integration. Now, to start capturing requests, run:

  • npx pytagora --initScript ./server.js where "./server.js" is the file (with relative path) you use to run your app

Now that capturing is running, open your app in a browser and click around your app as if you’re doing manual testing (or make requests manually if that suits you better).

Ok, once you've clicked around the app enough, stop the capturing and run the tests with:

  • npx pytagora --mode test --initScript ./server.js

All this will take you 10, 20 minutes and you can have a 100+ tests ready to go. To capture more tests, you just need to start the capturing command and click around your app to cover endpoints or cases that you haven't covered yet.

See how this all works in practice in the following video.

<YOUTUBE LINK>

We’re using Pythagora internally for our other web apps and we’re getting 80% coverage already with 30 minutes of clicking around the app.

If you don’t like writing and maintaining automated tests, give Pythagora a try, it will make your life a lot easier!

Pythagora Alpha

I hope you now have a better understanding of the importance of API testing and how you can implement it in your Node.js application. As I mentioned earlier, Pythagora is a package that aims to make this task easier. Currently, it is in an alpha stage and it supports only Node.js apps that use Express and Mongoose, which should be many MERN apps.

We are looking to expand Pythagora to cover more technologies and add features like updating tests when something changes in the response from an endpoint.

This is where we need your help. We would love to hear from you and know your use cases where you find Pythagora useful. And what would you want to see in this package?

Our brief roadmap in the following months is currently:

  • Enable code coverage for ES6 repositories
  • Reviewing updates to endpoints (git-like review of changes that make tests fail - similar to Jest snapshot testing review)
  • Making script testing autonomous (basically the same as API endpoint testing but for scripts that run from command line)
  • Support more databases (PostgreSQL likely being the first in line)

Please feel free to reach out to us, we are always looking for feedback to improve and make Pythagora the first truly autonomous testing package for Node.js. Thanks for reading!

P.S. We would love if you could star the Pythagora repository on Github.