No one should need Postman/Insomnia to test an endpoint

No one should need Postman/Insomnia to test an endpoint

Intro

Hey, how's it going? I'm Erick Wendel and today I'm going to show you an incredible technique to improve your productivity and, at the same time, improve the quality of your application delivery.

I saw this tweet (in Brazilian Portuguese) last week where the author said that no one should need Postman/Insomnia to test an endpoint. I agree with his opinion 100% and in this post, I'll show you why with practical examples and a project that you can replicate in your daily work.

Are you ready to see it?

Why not?

Postman and Insomnia are extremely useful tools to help us consume Web APIs. In our daily work, we usually put the URL of the project we're developing to validate what the application returns.

The problem is that this process becomes repetitive. You make a change in your code, restart the server, go to Postman, change the parameters that will be sent to the Web API, send the request, and check the result.

And then, you notice that the API returned a wrong result, very different from what you wanted. So, you modify the program and repeat the whole process.

This vicious cycle makes you waste A LOT of time, as there are several steps between restarting the server, changing the tool, and sending the request.

There is a much better way that also improves your software quality. Instead of manually testing your application with these tools, you can create automated tests.

With automated tests, the process is much less painful. You define what you expect that endpoint to return, and for every change you make in the code, the test suite is restarted, and you see the result instantly on the screen 🤯

Not only do you save time, but you also enable an important possibility. Since all your endpoints are properly tested, you can include a Continuous Integration pipeline.

Continuous Integration is an extremely important and widely used process out there. Imagine the following: your application has been working for months, and some tired developer went there and uploaded broken code.

The continuous integration pipeline will run the tests you had already created and confirm that something went wrong, blocking that update.

Thus, your users will never suffer from critical bugs or red screens in their applications, you get a good night's sleep, and the software grows healthily.

So, that's what I'm going to show you now. To make it a bit more challenging, I'll take an already functional application with some endpoints and implement new routes.

I'll show you how this process is done in Postman and how painful this trial and error can be. I'll also show you how tests will improve your quality of life as a developer.

IMPORTANT: The testing technique I'm going to show you today is called end-to-end testing. That means you're going to test the application from the point of view of a user who doesn't know the application's internal implementation.

This is extremely useful for validating the application as a whole, but over time, test automation can take a long time to run, and it's important to balance it with unit tests.

I have a video on this channel talking about unit tests as well but don't worry, for today's class, you don't need any prior knowledge.

I want to open your mind to a world of possibilities where you code faster and still build quality software.

Enough talking, let's get started!

Using Postman - Understanding the problem

The problem is not using Postman itself, as I said before the idea here is to get a routine where you can write code, get instant feedback, and still deliver a maintainable codebase.

Workflow while using Postman and similars

When we use those kinds of tools we generally follow these steps:

  1. Write the server and some Web API route

  2. Go to the postman and make an HTTP request

  3. Change the code in the backend

  4. Reload the server

  5. Go back to Postman and make another request

To save time, you'd create some testing scripts on Postman to save the token and put it on every request through environment variables such as:

And when you need to request a private route you'd refer to the variable on the Request Headers such as:

Still, as the project grows, it would be messy, and a bunch of URLs and variables being defined there.

Going back and forth to the application interface to check logs, debug code results, and validate a workflow makes us waste a lot of time.

Postman is not a tool to make E2E tests and as far as I know, we can't run a Continuous Integration process using it.

What if we could see the results instantly directly from our software development environment and still save time while doing it?

Getting Started

I've created a small code template with a few tests to simulate how to implement tests on an existing application.

IMPORTANT: This post doesn't focus on best practices, the goal is to show you how you can improve productivity and write tests on your existing applications.

Go there and clone this repo. This project uses Node.js v19 as you'll be using the latest Node.js features for testing and watching for hot reload.

This repo is part of my other blog post on Node.js e2e testing, you'd take a look later to get some context.

Making sure the template is ok

After restoring the dependencies with npm ci and making sure you're using Node.js in the v19.8 using node -v run npm run test to make sure everything is working as expected.

You should see the output below:

npm run dev

> app@0.0.1 dev
> node --watch api.js

(node:20514) ExperimentalWarning: Watch mode is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
listening to 3000

Let's take a look at what is there.

// api.js
async function loginRoute(request, response) {
  const { user, password } = JSON.parse(await once(request, "data"))
  if (user !== VALID.user || password !== VALID.password) {
    response.writeHead(400)
    response.end(JSON.stringify({ error: 'user invalid!' }))
    return
  }

  const token = jsonwebtoken.sign({ user, message: 'heyduude' }, TOKEN_KEY)

  response.end(JSON.stringify({ token }))
}

This template will authenticate users and return a JWT Token so consumers and use it to make requests to private routes.

Looking at the code on the template you'll see that there're two routes defined there the /login and whatever other route you try accessing will either return not found if requested with a valid token or an invalid token error if the opposite.

// api.js
async function handler(request, response) {
  if (request.url === '/login' && request.method === "POST") {
    return loginRoute(request, response)
  }

  if (!validateHeaders(request.headers)) {
    response.writeHead(400)
    return response.end("invalid token!")
  }

  response.writeHead(404)
  response.end('not found!')
}

You're gonna write tests for all existing code and then you're gonna implement a new route to create products.

Understanding the business requirements

To make things more interesting I'll define below the requirements for the route you're gonna implement:

  1. The POST /product will receive the product's description and price from the request and return what category this product belongs to.

  2. If the product price is higher than 100 the endpoint will respond with the premium category

  3. if the product price is between 50 and 99, the endpoint will respond with the regular category

  4. if the product price is less than 50, the endpoint will respond with the basic category

The product route is a private route which means if a consumer wants to access it, they must send a valid JWT Token on the Request headers.

Setting up the testing environment

Create an empty file called api.test.js in the same folder as your package.json file is located.

Head to the package.json and add the scripts as follows:

  "scripts": {
    "dev": "node --watch api.js",
    "test:dev": "node --watch --test api.test.js",
    "test": "node --test api.test.js"
  },

If you're not familiar with the Node.js native test runner, I recommend you take a look at the recent post I wrote on How to create E2E tests in Node.js here in the blog. But don't worry it's not a requirement for following this tutorial.

Go to your terminal and run npm run test:dev the output should be as follows:

npm run test:dev

> app@0.0.1 test:dev
> node --watch --test api.test.js

✔ /Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js (38.097208ms)

You're all set to start creating tests.

You're gonna need to import the app.js file in this test so you'd spin up the server and start making requests.

Notice that you'll be using the watch mode which means you must make sure that before the code is restarted you'd stop the server and prevent errors of two program instances trying to run in the same port.

In the api.test.js file, paste the code below:

import { describe, before, after, it } from 'node:test'
import { deepEqual, deepStrictEqual, ok } from 'node:assert'
const BASE_URL = `http://localhost:3000`
describe('API Products Test Suite', () => {
  let _server = {}
  let _globalToken = ''

  before(async () => {
    _server = (await import('./api.js')).app

    await new Promise(resolve => _server.once('listening', resolve))
  })
  after(done => _server.close(done))
})

The code above will import the API.js file and wait until the server is ready to receive requests using the before function.

Using the after function, you make sure that after the entire testing suite has been executed it'll close the server.

Your terminal now should look like this output:

npm run test:dev

> app@0.0.1 test:dev
> node --watch --test api.test.js

✔ /Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js (38.097208ms)
ℹ listening to 3000
✔ API Products Test Suite (54.460625ms)

This means that the server is being started and everything is working as expected.

Writing E2E Tests

As it's required for all routes but the login route to pass through a JWT Token we'll add a new before step which will request the API, get the token and store it on the _globalToken variable.

Inside the describe block paste the code below:

 async function makeRequest(url, data) {
    const request = await fetch(url, {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        authorization: _globalToken
      }
    })
    deepEqual(request.status, 200)
    return request.json()
  }
  async function setToken() {
    const input = {
      user: 'erickwendel',
      password: '123'
    }

    const data = await makeRequest(`${BASE_URL}/login`, input)
    ok(data.token, 'token should be present')
    _globalToken = data.token
  }

Notice how straightforward is this code. You're using the fetch which is a global function used to make requests.

In the past, we usually had to install third libraries such as supertest but it's not needed to install any library as you'd do using only the fetch module.

And right after the existing before function call, you'd add a new before function calling the setToken function you just pasted.

The end of the file will look like the below:

  before(async () => {
    _server = (await import('./api.js')).app

    await new Promise(resolve => _server.once('listening', resolve))
  })
  before(async () => setToken())
  after(done => _server.close(done))

This will make sure the _globalToken variable will always be populated before hitting the endpoints.

You haven't implemented the /product route yet. Before implementing it let's implement the test first.

  it('it should create a premium product', async () => {
    const input = {
      description: "tooth brush",
      price: 101
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "premium")
  })

Your terminal should now show an error such as:

▶ API Products Test Suite
  ✖ it should create a premium product (6.095959ms)
    AssertionError: Expected values to be loosely deep-equal:

    404

    should loosely deep-equal

    200
        at makeRequest (file:///Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js:15:5)
        at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
        at async TestContext.<anonymous> (file:///Users/erickwendel/Downloads/projetos/postman-is-slowing-you-down/template/api.test.js:39:18)
        at async Test.run (node:internal/test_runner/test:548:9)
        at async Promise.all (index 0)
        at async Suite.run (node:internal/test_runner/test:801:7)
        at async startSubtest (node:internal/test_runner/harness:192:3) {
      generatedMessage: false,
      code: 'ERR_ASSERTION',
      actual: 404,
      expected: 200,
      operator: 'deepEqual'
    }

▶ API Products Test Suite (86.56325ms)

Now you would implement it! Let's head back to the api.js file and implement this route.

First let's create the function with all the business requirements listed in the previous section:

// api.js
async function createProductRoute(request, response) {
  const { description, price } = JSON.parse(await once(request, "data"))
    const categories = {
      premium: {
        from: 101,
        to: 500
      },
      regular: {
        from: 51,
        to: 100
      },
      basic: {
        from: 0,
        to: 50
      },
    }
    const category = Object.keys(categories).find(key => {
      const category = categories[key]
      return price >= category.from && price <= category.to
    })
    response.end(JSON.stringify({ category }))
}

Then let's jump to the handler function and implement the logic for receiving POST requests related to this endpoint.

The full implementation for the handler function will look like below:

// api.js
async function handler(request, response) {
  if (request.url === '/login' && request.method === "POST") {
    return loginRoute(request, response)
  }

  if (!validateHeaders(request.headers)) {
    response.writeHead(404)
    return response.end("invalid token!")
  }

  if (request.url == '/products' && request.method === "POST") {
    return createProductRoute(request, response)
  }

  response.writeHead(404)
  response.end('not found!')
}

Now, checking the output in the terminal the test should pass

npm run test:dev

> app@0.0.1 test:dev
> node --watch --test api.test.js

ℹ listening to 3000
▶ API Products Test Suite
  ✔ it should create a premium product (4.854667ms)
▶ API Products Test Suite (79.295167ms)

Niiiiiiiiiiiiice! Now it's the easiest part. Let's implement all the tests for the remaining business logic. Jump to the api.test.js and paste the test cases below after the last it.

  it('it should create a regular product', async () => {
    const input = {
      description: "escova de dente",
      price: 70
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "regular")

  })

  it('it should create a basic product', async () => {
    const input = {
      description: "fio",
      price: 2
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "basic")

  })

All tests should pass

▶ API Products Test Suite
  ✔ it should create a premium product (12.418709ms)
  ✔ it should create a regular product (3.384458ms)
  ✔ it should create a basic product (2.473792ms)
▶ API Products Test Suite (100.180334ms)

Nice isn't it?

In case you're struggling in some part here is the full code for the api.js file

import jsonwebtoken from 'jsonwebtoken'
import { once } from 'node:events'
import { createServer } from 'node:http'

const VALID = {
  user: 'erickwendel',
  password: '123'
}
const TOKEN_KEY = "abc123"

async function loginRoute(request, response) {
  const { user, password } = JSON.parse(await once(request, "data"))
  if (user !== VALID.user || password !== VALID.password) {
    response.writeHead(400)
    response.end(JSON.stringify({ error: 'user invalid!' }))
    return
  }

  const token = jsonwebtoken.sign({ user, message: 'heyduude' }, TOKEN_KEY)

  response.end(JSON.stringify({ token }))
}

function validateHeaders(headers) {
  try {
    const auth = headers.authorization.replace(/bearer\s/ig, '')
    jsonwebtoken.verify(auth, TOKEN_KEY)
    return true
  } catch (error) {
    return false
  }
}
async function createProductRoute(request, response) {
  const { description, price } = JSON.parse(await once(request, "data"))
    const categories = {
      premium: {
        from: 101,
        to: 500
      },
      regular: {
        from: 51,
        to: 100
      },
      basic: {
        from: 0,
        to: 50
      },
    }
    const category = Object.keys(categories).find(key => {
      const category = categories[key]
      return price >= category.from && price <= category.to
    })
    response.end(JSON.stringify({ category }))
}
async function handler(request, response) {
  if (request.url === '/login' && request.method === "POST") {
    return loginRoute(request, response)
  }

  if (!validateHeaders(request.headers)) {
    response.writeHead(400)
    return response.end("invalid token!")
  }

  if (request.url == '/products' && request.method === "POST") {
    return createProductRoute(request, response)
  }

  response.writeHead(404)
  response.end('not found!')
}

const app = createServer(handler)
  .listen(3000, () => console.log('listening to 3000'))

export { app }

And also for the api.test.js file

import { describe, before, after, it } from 'node:test'
import { deepEqual, deepStrictEqual, ok } from 'node:assert'
const BASE_URL = `http://localhost:3000`
describe('API Products Test Suite', () => {
  let _server = {}
  let _globalToken = ''
  async function makeRequest(url, data) {

    const request = await fetch(url, {
      method: 'POST',
      body: JSON.stringify(data),
      headers: {
        authorization: _globalToken
      }
    })
    deepEqual(request.status, 200)
    return request.json()
  }
  async function setToken() {
    const input = {
      user: 'erickwendel',
      password: '123'
    }

    const data = await makeRequest(`${BASE_URL}/login`, input)
    ok(data.token, 'token should be present')
    _globalToken = data.token
  }
  before(async () => {
    _server = (await import('./api.js')).app

    await new Promise(resolve => _server.once('listening', resolve))
  })
  before(async () => setToken())
  it('it should create a premium product', async () => {
    const input = {
      description: "tooth brush",
      price: 101
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "premium")

  })
  it('it should create a regular product', async () => {
    const input = {
      description: "escova de dente",
      price: 70
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "regular")

  })

  it('it should create a basic product', async () => {
    const input = {
      description: "fio",
      price: 2
    }
    const data = await makeRequest(`${BASE_URL}/products`, input)
    deepStrictEqual(data.category, "basic")

  })
  after(done => _server.close(done))
})

Wrapping Up

You start seeing it paying off as any change you make to your code will instantly reflect on your terminal. You won't need to go back and forth to the Postman or similar.

Once you set up the testing environment you copy and paste the testing cases and change the input and expected result.

You save a lot of time and any new developer who wants to get to know what endpoints and how to access them would access the testing environment and test the code as a whole.

As you've written tests, you'd add a Continuous Integration step, such as the GitHub Actions.

This is important for testing all endpoints and checking whether the new code or new releases will break any part of your application and prevent it from crashing on the face of your users.

Homework

Your homework is to cover the remaining endpoints and add more features to the application and refactor this ugly code.

Now that you have understood how the step-by-step process works, I encourage you to try testing existing applications in your work and cover the main paths that clients access.

This is a great way to learn and improve your skills in JavaScript.

Thanks for reading

I am always available to answer your questions in the comments section. Also, let me know in the comments what kind of content you'd like to see next.

I hope you enjoyed this post and found it helpful please give it a thumbs up in this post as it helps me a lot.

I hope this content has exceeded your expectations. Thank you very much for reading, and I'm Erick Wendel. See you in the next post!