DEV Community

Bentil Shadrack
Bentil Shadrack

Posted on

Effective Unit Testing for REST APIs with Node.js, TypeScript

REST APIs, or Representational State Transfer Application Programming Interfaces, are a set of rules that allow programs to communicate with each other over the internet. They use standard HTTP methods like GET, POST, PUT, and DELETE to perform operations. For instance, when you log in to a website, submit a form, or retrieve data, you're likely interacting with a REST API. They are foundational to modern web applications, enabling seamless interaction between client-side applications and server-side logic.

rest api

The Importance of Unit Testing

Unit testing involves testing individual pieces of code, such as functions or methods, to ensure they work correctly. For REST APIs, unit testing is crucial because it:

  • Ensures Correctness: Confirms that each endpoint behaves as expected.
  • Detects Issues Early: Catches bugs early in the development process, making them easier and cheaper to fix.
  • Facilitates Refactoring: Allows developers to change code without fear of breaking existing functionality.
  • Enhances Maintainability: Makes the codebase easier to maintain and extend, as tests provide a safety net.
  • Supports CI/CD: Integrates with Continuous Integration/Continuous Deployment pipelines to ensure ongoing code quality.

Tools and Technologies: Node.js, TypeScript, and Jest

In this guide, we'll be using:

  • Node.js: A JavaScript runtime built on Chrome's V8 engine, ideal for building fast and scalable network applications.
  • TypeScript: A statically typed superset of JavaScript that adds types, enhancing code quality and developer productivity.
  • Jest: A delightful JavaScript testing framework with a focus on simplicity, providing powerful tools for writing and running tests.

Boost Your Tests with Codium AI's Cover-Agent

Test generation xxx

Introducing Codium AI's Cover-Agent, a powerful tool designed to boost your test coverage without the stress. The Cover-Agent simplifies and automates the generation of tests using advanced Generative AI models, making it easier to handle critical tasks like increasing test coverage. Key features include:

  • Test Generation Technology: Automates the creation of regression tests.
  • Open-Source Collaboration: Continuously improved through community contributions.
  • Streamlined Development Workflows: Runs via a terminal and integrates with popular CI platforms.
  • Comprehensive Suite of Tools: Includes components like Test Runner, Coverage Parser, Prompt Builder, and AI Caller to ensure high-quality software development.

surprised

With Cover-Agent, you can focus on developing features while it takes care of generating and enhancing your tests, ensuring your APIs remain reliable and maintainable.
You can easily get started from their GitHub repository

What is Unit Testing?

Unit testing is a software testing technique where individual components or units of a program are tested in isolation from the rest of the system. A "unit" refers to the smallest testable part of any software, which could be a function, method, procedure, module, or object. The goal of unit testing is to validate that each unit of the software performs as expected.

Purpose and Benefits of Unit Testing

Unit testing serves several important purposes and offers numerous benefits:

  1. Ensures Code Correctness: By testing each part of the code independently, you can verify that the logic within individual units is correct.
  2. Early Detection of Bugs: Unit tests can catch bugs at an early stage, which is often easier and less expensive to fix compared to issues found later in the development cycle.
  3. Facilitates Refactoring: With a comprehensive suite of unit tests, developers can refactor or update code with confidence, knowing that the tests will catch any regressions or errors introduced.
  4. Simplifies Integration: By ensuring that each unit works correctly in isolation, the integration of various parts of the system becomes smoother and less error-prone.
  5. Documentation: Unit tests act as documentation for the code. They describe how the code is supposed to behave, making it easier for new developers to understand the system.
  6. Improves Code Quality: Writing unit tests encourages developers to write better-structured, more maintainable, and testable code.
  7. Supports Continuous Integration/Continuous Deployment (CI/CD): Automated unit tests can be run as part of the CI/CD pipeline, ensuring that any new code changes do not break existing functionality.

Differentiating Unit Testing from Other Types of Testing

  1. Unit Testing vs. Integration Testing

    • Unit Testing: Focuses on testing individual units or components in isolation.
    • Integration Testing: Focuses on testing the interaction between different units or components to ensure they work together as expected.
  2. Unit Testing vs. System Testing

    • Unit Testing: Tests the smallest parts of an application independently.
    • System Testing: Tests the entire system as a whole to ensure that it meets the specified requirements.
  3. Unit Testing vs. End-to-End (E2E) Testing

    • Unit Testing: Ensures that each individual part of the application functions correctly.
    • End-to-End Testing: Simulates real user scenarios to ensure that the entire application flow, from start to finish, works as expected.

By understanding these differences, you can see that unit testing forms the foundation of a comprehensive testing strategy, ensuring that the building blocks of your application are solid and reliable before moving on to more complex integration, system, and end-to-end tests.

Now that we understand what REST APIs are and why unit testing is crucial for maintaining reliable and maintainable APIs, let's get our hands dirty with a step-by-step approach. Don't worry if you've never written a test before; we'll guide you through the entire process from scratch. By the end of this guide, you'll have a solid grasp of how to set up and write unit tests for your REST APIs using Node.js, TypeScript, and Jest. Let's dive in!

get started

Setting Up the Environment

  • Node.js and TypeScript: Provide installation steps and basic configuration.
  • Jest: Guide on installing Jest and setting it up with TypeScript.
npm init -y
npm install typescript ts-jest @types/jest jest --save-dev
npx ts-jest config:init
Enter fullscreen mode Exit fullscreen mode
  • Basic TypeScript Configuration: Include a sample tsconfig.json.
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Writing Unit Tests for REST APIs

  1. Identify Test Cases

    • List common scenarios to test (e.g., successful responses, error responses, edge cases).
  2. Mocking Dependencies

    • Explain the concept of mocking with Jest.
    • Demonstrate how to mock external dependencies using jest.mock.
  3. Creating Test Data

    • Discuss strategies for creating reliable and reusable test data.
  4. Writing the Tests

    • Show examples of unit test cases for various endpoints (GET, POST, PUT, DELETE).
    • Cover both positive and negative test cases.

Example: Unit Testing a Sample REST API

Provide a simple REST API example, such as a CRUD API for managing users.

Sample Project Structure:

/src
  /controllers
    userController.ts
  /services
    userService.ts
  /models
    user.ts
  /routes
    userRoutes.ts
  app.ts
/server
  server.ts
/tests
  userController.test.ts
  userService.test.ts
Enter fullscreen mode Exit fullscreen mode

Sample Code:

  • app.ts: ```js import express from 'express'; import userRoutes from './routes/userRoutes';

const app = express();
app.use(express.json());
app.use('/users', userRoutes);

export default app;


- **server.ts:**
```js
import app from '../src/app';

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode
  • userController.ts: ```js import { Request, Response } from 'express'; import { getUsers, createUser } from '../services/userService';

export const getAllUsers = async (req: Request, res: Response) => {
const users = await getUsers();
res.json(users);
};

export const addUser = async (req: Request, res: Response) => {
const user = req.body;
const newUser = await createUser(user);
res.status(201).json(newUser);
};


- **userService.ts:**
```js
import { User } from '../models/user';

const users: User[] = [];

export const getUsers = async (): Promise<User[]> => {
  return users;
};

export const createUser = async (user: User): Promise<User> => {
  users.push(user);
  return user;
};
Enter fullscreen mode Exit fullscreen mode
  • user.ts:

    export interface User {
    id: number;
    name: string;
    email: string;
    }
    
  • userRoutes.ts:

    import { Router } from 'express';
    import { getAllUsers, addUser } from '../controllers/userController';
    

const router = Router();

router.get('/', getAllUsers);
router.post('/', addUser);

export default router;


Writing Tests with Jest

  • userController.test.ts: ```js import request from 'supertest'; import app from '../src/app'; import * as userService from '../src/services/userService'; import { User } from '../src/models/user';

jest.mock('../src/services/userService');

describe('User Controller', () => {
describe('GET /users', () => {
it('should return a list of users', async () => {
const mockUsers: User[] = [{ id: 1, name: 'John Doe', email: 'john@example.com' }];
(userService.getUsers as jest.Mock).mockResolvedValue(mockUsers);

  const response = await request(app).get('/users');

  expect(response.status).toBe(200);
  expect(response.body).toEqual(mockUsers);
});
Enter fullscreen mode Exit fullscreen mode

});

describe('POST /users', () => {
it('should create a new user', async () => {
const newUser: User = { id: 2, name: 'Jane Doe', email: 'jane@example.com' };
(userService.createUser as jest.Mock).mockResolvedValue(newUser);

  const response = await request(app)
    .post('/users')
    .send(newUser);

  expect(response.status).toBe(201);
  expect(response.body).toEqual(newUser);
});
Enter fullscreen mode Exit fullscreen mode

});
});

Enter fullscreen mode Exit fullscreen mode




Best Practices for Unit Testing REST APIs

  • Keep tests isolated and independent.
  • Use descriptive test names.
  • Aim for high test coverage but focus on quality over quantity.
  • Regularly review and update tests.
  • Integrate tests into the CI/CD pipeline.

Conclusion

In this article, I explored the importance of unit testing for REST APIs, emphasizing its role in ensuring reliable and maintainable applications. I covered the basics of unit testing, its benefits, and how it differs from other types of testing.

Using Node.js, TypeScript, and Jest, I provided a step-by-step guide on setting up and writing unit tests for REST APIs. Thorough unit testing helps catch bugs early, simplifies refactoring, and improves code quality.

I encourage you to implement unit tests in your projects to achieve these benefits. Additionally, consider using Codium AI's Cover-Agent to automate and enhance your test generation process, making it easier to maintain high test coverage and code quality.

happy

Happy testing!

Additional Resources

gif

Bentil here🚀
Are you familiar with writing unit tests using jest? Which other frameworks or approach have you been using? Kindly share your experience with them in the comments. This will help others yet to use them.

Top comments (2)

Collapse
 
guilherme_valentim_1e1386 profile image
Guilherme Valentim • Edited

Great job on the work! Very nice post.

For future readers, I recommend using "esnext" instead of "commonjs" in your tsconfig.json, especially since the examples are written using ES modules. Using "commonjs" might lead to errors when trying to run the generated .js file. Another change I would suggest is to use tsx instead of ts-node, because ts-node sometimes can get very stressful with non-sense errors.

So you could have something like:

package.json

//...
  "scripts": {
    "build": "tsc",
    "start": "node dist/server/server.js",
    "dev": "tsx server/server.ts"
  },
//...
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "compilerOptions": {
    "target": "es6",
    "module": "esnext",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "noEmit": true,
    "allowImportingTsExtensions": true

    //"baseUrl": "./src",
    //"paths": {
    //  "*": ["node_modules/*", "src/types/*"]
    //}
  },
  "include": ["src/**/*.ts"],
  "include": ["server/**/*.ts"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

In this way, you can run your .ts files while coding in dev by simply executing npm run dev, and you can run your transpiled .js files by running npm run build, and then npm start.

Collapse
 
qbentil profile image
Bentil Shadrack

Thank you for the update, Valentim📌