DEV Community

Cover image for Unit Testing in NodeJS with Express, TypeScript, Jest and Supertest || Part Three: Writing Unit Tests || A Comprehensive Guide
Abeinemukama Vicent
Abeinemukama Vicent

Posted on

Unit Testing in NodeJS with Express, TypeScript, Jest and Supertest || Part Three: Writing Unit Tests || A Comprehensive Guide

Welcome back to the third installment of our series on building a powerful API with Node.js, Express, and MongoDB and writing unit tests for each line of code! In the previous 2 parts: part one and part two, we set up the environment and meticulously crafted our application, ensuring robust user authentication and seamless database interactions. Now, it's time to fortify our codebase and elevate our development practices through the implementation of thorough unit tests.
In this part, we'll embark on a step-by-step journey through the process of writing unit tests for each line of code in our API. From utility functions, routes, controllers to services and models, we'll ensure that every aspect of our application is rigorously tested.

Why Unit Testing Matters

Unit testing plays a pivotal role in the development lifecycle, offering numerous benefits, including:

Early Issue Detection:

Identify and address bugs and issues early in the development process, reducing the likelihood of encountering them in later stages.

Code Maintainability:

Establish a safety net for code changes by ensuring that existing functionalities remain intact during modifications.

Documentation:

Unit tests serve as living documentation, showcasing the expected behavior of each component, function, or module.

Enhanced Collaboration:

Facilitate collaboration among team members by providing a clear understanding of the intended functionality and expected outcomes.

Folder Structure for Unit Tests

Organizing your unit tests is crucial for clarity and ease of maintenance. Consider adopting a structure similar to your application's folder layout. For our API, I recommend the following structure:

Image description

Just like I promised, we will be writing unit tests for each line of code. In the first part of this guide, after setting up the environment, we wrote some tests in the app.test.ts but by then we didn't have all the code in the index file like we do now, lets first update that file and make sure everything we have in the src/index.ts is working as expected including datababase connection to MongoDB Atlas, among others before starting with models.
Replace code inside app.test.ts with the following code:

import request from "supertest";
import app from "../index";
import mongoose from "mongoose";

beforeAll(async () => {
  // Set up: Establish the MongoDB connection before running tests
  if (!process.env.MONGODB_URL) {
    throw new Error("MONGO_URI environment variable is not defined/set");
  }

  await mongoose.connect(process.env.MONGODB_URL);
});

afterAll(async () => {
  // Teardown: Close the MongoDB connection after all tests have completed
  await mongoose.connection.close();
});

// Unit test for testing initial route ("/")
describe("GET /", () => {
  it('responds with "Welcome to unit testing guide for nodejs, typescript and express!"', async () => {
    const response = await request(app).get("/");

    expect(response.status).toBe(200);
    expect(response.text).toBe(
      "Welcome to unit testing guide for nodejs, typescript and express!"
    );
  });
});

Enter fullscreen mode Exit fullscreen mode

Our app.test.ts file orchestrates the setup and teardown of the testing environment, establishing a MongoDB connection before tests begin and closing it afterward. The primary test case verifies the behavior of the root route ("/"), ensuring it responds with a welcome message and a 200 status. The code includes error handling to prompt the developer if the MongoDB connection string (MONGODB_URL) is not defined. This file serves as a foundational template for testing the core functionalities of the application, paving the way for more extensive unit testing across various directories and components.

Open your terminal and run:

npm run test
Enter fullscreen mode Exit fullscreen mode

The output should be as shown below:

Image description

All set, now ready to write unit tests for our code in other directories.

Unit Tests for Database Models

Lets start with the data layer by writing unit tests for our database models.
Testing models allows us to validate the structure, functionality, and interactions with the database. By establishing robust tests for our models, we create a solid base upon which the rest of our unit tests can thrive.
The beforeAll hook orchestrates the setup process before running any tests, ensuring a valid MongoDB connection is established based on the MONGODB_URL environment variable. It throws an error if the variable is not defined, alerting the developer to set the MongoDB connection string. On the other hand, the afterAll hook handles cleanup after all tests are completed, closing the MongoDB connection to prevent resource leaks and maintain a clean testing environment. These hooks collectively manage the initialization and teardown phases, ensuring a consistent and isolated testing environment for the unit tests.

Create a new file: src/tests/models/userModel.test.ts and place the following code:

import mongoose from "mongoose";
import User, { IUser } from "../../models/User";
import dotenv from "dotenv";
dotenv.config();

describe("User Model Tests", () => {
  let createdUser: IUser;

  beforeAll(async () => {
    // Set up: Establish the MongoDB connection before running tests
    if (!process.env.MONGODB_URL) {
      throw new Error("MONGODB_URL environment variable is not defined/set");
    }

    await mongoose.connect(process.env.MONGODB_URL);
  });

  afterAll(async () => {
    // Remove the created user
    // await User.deleteMany();

    // Teardown: Close the MongoDB connection after all tests have completed
    await mongoose.connection.close();
  });

  // Test Case: Create a new user
  it("should create a new user", async () => {
    const userData: Partial<Omit<IUser, "_id">> = {
      email: "test@example.com",
      username: "testuser",
      password: "testpassword",
      isAdmin: false,
      savedProducts: [],
    };

    createdUser = await User.create(userData);

    expect(createdUser.email).toBe(userData.email);
    expect(createdUser.username).toBe(userData.username);
    expect(createdUser.isAdmin).toBe(userData.isAdmin);
  }, 10000); // Increase timeout to 10 seconds

  // Test Case: Ensure email and username are unique
  it("should fail to create a user with duplicate email or username", async () => {
    const userData: Partial<Omit<IUser, "_id">> = {
      email: "test@example.com",
      username: "testuser",
      password: "testpassword",
      isAdmin: false,
      savedProducts: [],
    };

    try {
      // Attempt to create a user with the same email and username
      await User.create(userData);
      // If the above line doesn't throw an error, the test should fail
      expect(true).toBe(false);
    } catch (error) {
      // Expect a MongoDB duplicate key error (code 11000)
      expect(error.code).toBe(11000);
    }
  }, 10000); // Increase timeout to 10 seconds

  // Test Case: Get all users
  it("should get all users", async () => {
    // Fetch all users from the database
    const allUsers = await User.find();

    // Expectations
    const userWithoutTimestamps = {
      _id: createdUser._id,
      email: createdUser.email,
      username: createdUser.username,
      isAdmin: createdUser.isAdmin,
      savedProducts: createdUser.savedProducts,
    };

    expect(allUsers).toContainEqual(
      expect.objectContaining(userWithoutTimestamps)
    );
  });

  const removeMongoProps = (user: any) => {
    const { __v, _id, createdAt, updatedAt, ...cleanedUser } = user.toObject();
    return cleanedUser;
  };

  // Test Case: Get all users
  it("should get all users", async () => {
    const allUsers = await User.find();

    // If there is a created user, expect the array to contain an object
    // that partially matches the properties of the createdUser
    if (createdUser) {
      const cleanedCreatedUser = removeMongoProps(createdUser);

      expect(allUsers).toEqual(
        expect.arrayContaining([expect.objectContaining(cleanedCreatedUser)])
      );
    }
  });

  // Test Case: Update an existing user
  it("should update an existing user", async () => {
    // Check if there is a created user to update
    if (createdUser) {
      // Define updated data
      const updatedUserData: Partial<IUser> = {
        username: "testuser",
        isAdmin: true,
      };

      // Update the user and get the updated user
      const updatedUser = await User.findByIdAndUpdate(
        createdUser._id,
        updatedUserData,
        { new: true }
      );

      // Expectations
      expect(updatedUser?.username).toBe(updatedUserData.username);
      expect(updatedUser?.isAdmin).toBe(updatedUserData.isAdmin);
    }
  });

  // Test Case: Get user by ID
  it("should get user by ID", async () => {
    // Get the created user by ID
    const retrievedUser = await User.findById(createdUser._id);

    // Expectations
    expect(retrievedUser?.email).toBe(createdUser.email);
    expect(retrievedUser?.username).toBe(createdUser.username);
    // Add other properties that you want to compare

    // For example, if updatedAt is expected to be different, you can ignore it:
    // expect(retrievedUser?.updatedAt).toBeDefined();
  });

  // Test Case: Delete an existing user
  it("should delete an existing user", async () => {
    // Delete the created user
    await User.findByIdAndDelete(createdUser._id);

    // Attempt to find the deleted user
    const deletedUser = await User.findById(createdUser._id);

    // Expectations
    expect(deletedUser).toBeNull();
  });
});

Enter fullscreen mode Exit fullscreen mode

Our userModel.test.ts script contains a series of Jest test cases for the user model. Before running the tests, it establishes a connection to a MongoDB database specified in the environment variable MONGODB_URL. The test suite covers creating a new user, ensuring the uniqueness of email and username, retrieving all users, updating an existing user, fetching a user by ID, and finally, deleting an existing user. The tests use the User model from the src/models/User file and leverage various assertions to verify the expected behavior of these operations. Additionally, a utility function removeMongoProps is used to clean MongoDB-specific properties from user objects for precise comparisons. The script performs setup and teardown procedures to connect to and disconnect from the database, making it a comprehensive set of tests for the user model.
I always prefer to test all crud operations on the model at this stage to be sure but the main effort should be on the data since we are on the data layer.
Feel free to add more test cases on the model forxample the one that tests the data type of data received on each field and compare it with what the model has among others.
All good for our user model because all our tests passed under different test data. Its time to move to the product model.

Create a new file: src/__tests__/models/productModel.test.ts and place the following code:

import mongoose from "mongoose";
import Product, { IProduct } from "../../models/Product";
import dotenv from "dotenv";
dotenv.config();

describe("Product Model Tests", () => {
  let createdProduct: IProduct;

  beforeAll(async () => {
    // Set up: Establish the MongoDB connection before running tests
    if (!process.env.MONGODB_URL) {
      throw new Error("MONGODB_URL environment variable is not defined/set");
    }

    await mongoose.connect(process.env.MONGODB_URL);
  });

  afterAll(async () => {
    // Remove the created product
    // await Product.deleteMany();

    // Teardown: Close the MongoDB connection after all tests have completed
    await mongoose.connection.close();
  });

  // Test Case: Create a new product
  it("should create a new product", async () => {
    const productData: Partial<IProduct> = {
      title: "Test Product",
      description: "Product description",
      image: "https://testimage.png",
      category: "test category",
      quantity: "20 kgs",
      // You can add other fields
    };

    createdProduct = await Product.create(productData);

    expect(createdProduct.title).toBe(productData.title);
    expect(createdProduct.description).toBe(productData.description);
    // Add other expectations for additional fields
  }, 10000); // Increase timeout to 10 seconds

  // Test Case: Fail to create a product with missing required fields
  it("should fail to create a product with missing required fields", async () => {
    const productData: Partial<IProduct> = {
      // Omitting required fields
    };

    try {
      // Attempt to create a product with missing required fields
      await Product.create(productData);
      // If the above line doesn't throw an error, the test should fail
      expect(true).toBe(false);
    } catch (error) {
      // Expect a MongoDB validation error
      expect(error.name).toBe("ValidationError");
    }
  }, 10000); // Increase timeout to 10 seconds

  // Test Case: Get all products
  it("should get all products", async () => {
    // Fetch all products from the database
    const allProducts = await Product.find();

    // Expectations
    const productWithoutTimestamps = {
      //   _id: createdProduct._id,
      title: createdProduct.title,
      description: createdProduct.description,
      // Add other necessary fields
    };

    expect(allProducts).toContainEqual(
      expect.objectContaining(productWithoutTimestamps)
    );
  });

  const removeMongoProps = (product: any) => {
    const { __v, _id, createdAt, updatedAt, ...cleanedProduct } =
      product.toObject();
    return cleanedProduct;
  };

  // Test Case: Get all products
  it("should get all products", async () => {
    const allProducts = await Product.find();

    // If there is a created product, expect the array to contain an object
    // that partially matches the properties of the createdProduct
    if (createdProduct) {
      const cleanedCreatedProduct = removeMongoProps(createdProduct);

      expect(allProducts).toEqual(
        expect.arrayContaining([expect.objectContaining(cleanedCreatedProduct)])
      );
    }
  });

  // Test Case: Update an existing product
  it("should update an existing product", async () => {
    // Check if there is a created product to update
    if (createdProduct) {
      // Define updated data
      const updatedProductData: Partial<IProduct> = {
        title: "Test Product", // replace hre with your updated title
        // Update other necessary fields
      };

      // Update the product and get the updated product
      const updatedProduct = await Product.findByIdAndUpdate(
        createdProduct._id,
        updatedProductData,
        { new: true }
      );

      // Expectations
      expect(updatedProduct?.title).toBe(updatedProductData.title);
      // Add expectations for other updated fields
    }
  });

  // Test Case: Get product by ID
  it("should get product by ID", async () => {
    // Get the created product by ID
    const retrievedProduct = await Product.findById(createdProduct._id);

    // Expectations
    expect(retrievedProduct?.title).toBe(createdProduct.title);
    // Add other expectations for properties you want to compare
  });

  // Test Case: Delete an existing product
  it("should delete an existing product", async () => {
    // Delete the created product
    await Product.findByIdAndDelete(createdProduct._id);

    // Attempt to find the deleted product
    const deletedProduct = await Product.findById(createdProduct._id);

    // Expectations
    expect(deletedProduct).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode

Our Product Model Tests are well-structured and cover essential CRUD operations on our data layer. Once again, the beforeAll and afterAll hooks ensure a connection to the MongoDB database is established before the tests and closed afterward. The "Create a new product" test validates the creation of a product with specified data, while the "Fail to create a product with missing required fields" test checks that the model correctly enforces required fields. The "Get all products" tests ensure that products retrieved from the database match the expected properties, and the utility function removeMongoProps helps clean unnecessary MongoDB-specific properties. The "Update an existing product" and "Get product by ID" tests validate the update and retrieval of products by ID. Lastly, the "Delete an existing product" test confirms the successful deletion of a product.
Overall, our tests provide comprehensive coverage for your Product model, ensuring its correctness and reliability in a MongoDB environment.
All set, lets proceed to services:

Writing UnitTests for Services

Create a new file: src/__tests__/services/userService.test.ts and place the following code:

import * as userService from "../../services/userService";
import * as passwordUtils from "../../utils/passwordUtils";
import * as jwtUtils from "../../utils/jwtUtils";
import User, { IUser } from "../../models/User";
import mongoose from "mongoose";
import dotenv from "dotenv";
import * as productModel from "../../models/Product"; // Import the Product model
dotenv.config();

// Mock the Product model
jest.mock("../../models/Product", () => ({
  __esModule: true,
  default: {
    findById: jest.fn(),
  },
}));

// Mock the populateUser function
jest
  .spyOn(userService, "populateUser")
  .mockImplementation(async (user: IUser) => {
    // Mock the behavior of populateUser function
    return user; // You can replace this with your desired mock value
  });

describe("User Service Tests", () => {
  let createdUser: IUser;

  // Clean up after tests
  beforeAll(async () => {
    // Set up: Establish the MongoDB connection before running tests
    if (!process.env.MONGODB_URL) {
      throw new Error("MONGODB_URL environment variable is not defined/set");
    }

    await mongoose.connect(process.env.MONGODB_URL);
  });

  afterAll(async () => {
    // Remove the created user
    await User.deleteMany();

    // Teardown: Close the MongoDB connection after all tests have completed
    await mongoose.connection.close();

    // Clear all jest mocks
    jest.clearAllMocks();
  });

  // Mock the hashPassword function
  jest
    .spyOn(passwordUtils, "hashPassword")
    .mockImplementation(async (password) => {
      // Mocked hash implementation
      return password + "_hashed";
    });
  // Mock the jwtUtils' generateToken function
  jest
    .spyOn(jwtUtils, "generateToken")
    .mockImplementation(() => "mocked_token");

  // Test Case: Create a new user
  it("should create a new user", async () => {
    const userData: Partial<IUser> = {
      email: "test@example.com",
      username: "testuser",
      password: "testpassword",
      isAdmin: false,
      savedProducts: [],
    };

    createdUser = await userService.createUser(userData as IUser);

    // Expectations
    expect(createdUser.email).toBe(userData.email);
    expect(createdUser.username).toBe(userData.username);
    expect(createdUser.isAdmin).toBe(userData.isAdmin);
    expect(passwordUtils.hashPassword).toHaveBeenCalledWith(userData.password);
  }, 30000);

  // Test Case: Login user
  it("should login a user and generate a token", async () => {
    // Mock user data for login
    const loginEmail = "test@example.com";
    const loginPassword = "testpassword";

    // Mock the comparePassword function
    jest
      .spyOn(passwordUtils, "comparePassword")
      .mockImplementation(async (inputPassword, hashedPassword) => {
        return inputPassword === hashedPassword.replace("_hashed", "");
      });

    const { user, token } = await userService.loginUser(
      loginEmail,
      loginPassword
    );

    // Expectations
    expect(user.email).toBe(createdUser.email);
    expect(user.username).toBe(createdUser.username);
    expect(user.isAdmin).toBe(createdUser.isAdmin);
    expect(jwtUtils.generateToken).toHaveBeenCalledWith({
      id: createdUser._id,
      username: createdUser.username,
      email: createdUser.email,
      isAdmin: createdUser.isAdmin,
    });
    expect(token).toBe("mocked_token");
  }, 20000);

  const removeMongoProps = (user: any) => {
    const { __v, _id, createdAt, updatedAt, ...cleanedUser } = user.toObject();
    return cleanedUser;
  };

  // Test Case: Get all users
  it("should get all users", async () => {
    // Fetch all users from the database
    const allUsers = await userService.getAllUsers();

    // If there is a created user, expect the array to contain an object
    // that partially matches the properties of the createdUser
    if (createdUser) {
      const cleanedCreatedUser = removeMongoProps(createdUser);

      expect(allUsers).toEqual(
        expect.arrayContaining([expect.objectContaining(cleanedCreatedUser)])
      );
    }
  }, 20000);

  // Test Case: Delete an existing user
  it("should delete an existing user", async () => {
    // Delete the created user
    await User.findByIdAndDelete(createdUser._id);

    // Attempt to find the deleted user
    const deletedUser = await User.findById(createdUser._id);

    // Expectations
    expect(deletedUser).toBeNull();
  });
});

Enter fullscreen mode Exit fullscreen mode

and src/__tests__/services/producService.test.ts and place the following code:

import * as productService from "../../services/productService";
import Product, { IProduct } from "../../models/Product";
import mongoose from "mongoose";
import dotenv from "dotenv";
dotenv.config();

// Mock the Product model
jest.mock("../../models/Product", () => ({
  __esModule: true,
  default: {
    create: jest.fn(),
    find: jest.fn(),
    findById: jest.fn(),
    findByIdAndUpdate: jest.fn(),
    findByIdAndDelete: jest.fn(),
  },
}));

// Mock the product data
const productId = "mockedProductId";
const mockProduct: IProduct = {
  _id: productId,
  title: "Mocked Product",
  description: "A description for the mocked product",
  image: "mocked_image.jpg",
  category: "Mocked Category",
  quantity: "10",
  inStock: true,
} as IProduct;

// Use the toObject method to include additional properties
const mockProductWithMethods = {
  ...mockProduct,
  toObject: jest.fn(() => mockProduct),
};

// Mock the product retrieval by ID
(Product.findById as jest.Mock).mockResolvedValueOnce(mockProductWithMethods);

describe("Product Service Tests", () => {
  // Clean up after tests
  beforeAll(async () => {
    // Set up: Establish the MongoDB connection before running tests
    if (!process.env.MONGODB_URL) {
      throw new Error("MONGODB_URL environment variable is not defined/set");
    }

    await mongoose.connect(process.env.MONGODB_URL);
  });

  afterAll(async () => {
    // Teardown: Close the MongoDB connection after all tests have completed
    await mongoose.connection.close();

    // Clear all jest mocks
    jest.clearAllMocks();
  });

  // Test Case: Create a new product
  it("should create a new product", async () => {
    // Mock the product data
    const productData: Partial<IProduct> = {
      title: "Test Product",
      // Add other product properties based on your schema
    };

    // Mock the product creation
    (Product.create as jest.Mock).mockResolvedValueOnce({
      ...productData,
      _id: "mockedProductId", // Mocked product ID
    });

    // Create the product
    const createdProduct = await productService.createProduct(
      productData as IProduct
    );

    // Expectations
    expect(createdProduct.title).toBe(productData.title);
    // You can add more expectations based on your schema and business logic
  }, 20000);

  // Test Case: Get product by ID
  it("should get product by ID", async () => {
    // Mock product data
    const productId = "mockedProductId";
    const mockProduct: IProduct = {
      _id: productId,
      title: "Mocked Product",
      description: "A description for the mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    } as IProduct;

    // Mock the findById method of the Product model
    (Product.findById as jest.Mock).mockResolvedValueOnce(mockProduct);

    // Call the service
    const retrievedProduct = await productService.getProductById(productId);

    // Expectations
    expect(retrievedProduct).toEqual(
      expect.objectContaining({
        _id: mockProduct._id,
        title: mockProduct.title,
        description: mockProduct.description,
        image: mockProduct.image,
        category: mockProduct.category,
        quantity: mockProduct.quantity,
        inStock: mockProduct.inStock,
      })
    );
    expect(Product.findById).toHaveBeenCalledWith(productId);
  }, 20000);

  // Test Case: Update product by ID
  it("should update product by ID", async () => {
    // Mock product data
    const productId = "mockedProductId";
    const mockProduct: IProduct = {
      _id: productId,
      title: "Mocked Product",
      description: "A description for the mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    } as IProduct;

    // Mock the findByIdAndUpdate method of the Product model
    (Product.findByIdAndUpdate as jest.Mock).mockResolvedValueOnce(mockProduct);

    // Mock updated product data
    const updatedProductData: Partial<IProduct> = {
      title: "Mocked Product", // update some fields
      quantity: "10",
    };

    // Call the service
    const updatedProduct = await productService.updateProduct(
      productId,
      updatedProductData
    );

    // Expectations
    expect(updatedProduct?._id).toBe(mockProduct._id);
    expect(updatedProduct?.title).toBe(updatedProductData.title);
    expect(updatedProduct?.quantity).toBe(updatedProductData.quantity);
    // Add similar expectations for other properties you want to compare

    expect(Product.findByIdAndUpdate).toHaveBeenCalledWith(
      productId,
      updatedProductData,
      { new: true }
    );
  }, 20000);

  // Test Case: Delete product by ID
  it("should delete product by ID", async () => {
    // Mock product data
    const productId = "mockedProductId";
    const mockProduct: IProduct = {
      _id: productId,
      title: "Mocked Product",
      description: "A description for the mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    } as IProduct;

    // Mock the findByIdAndDelete method of the Product model
    (Product.findByIdAndDelete as jest.Mock).mockResolvedValueOnce(mockProduct);

    // Call the service
    await productService.deleteProduct(productId);

    // Expectations
    expect(Product.findByIdAndDelete).toHaveBeenCalledWith(productId);
  }, 20000);

  // Test Case: Get all products
  it("should get all products", async () => {
    // Mock product data
    const mockProducts: IProduct[] = [
      {
        _id: "product1",
        title: "Product 1",
        description: "Description for Product 1",
        image: "product1_image.jpg",
        category: "Category 1",
        quantity: "5",
        inStock: true,
      },
      {
        _id: "product2",
        title: "Product 2",
        description: "Description for Product 2",
        image: "product2_image.jpg",
        category: "Category 2",
        quantity: "10",
        inStock: false,
      },
    ] as IProduct[];

    // Mock the find method of the Product model
    (Product.find as jest.Mock).mockResolvedValueOnce(mockProducts);

    // Call the service
    const retrievedProducts = await productService.getAllProducts();

    // Expectations
    expect(Product.find).toHaveBeenCalled();
    expect(retrievedProducts).toEqual(mockProducts);
  }, 20000);
});

Enter fullscreen mode Exit fullscreen mode

Our createUser service uses two external functions; hashPassword utility and generateToken middleware, all in src/utils. Lets first diagonise these utility functions and discuss their unit tests that this service is mocking.
Create a new file: src/__tests__/utils/passwordUtils.test.ts and place the following code:

import * as passwordUtils from "../../utils/passwordUtils";

describe("Password Utilities Tests", () => {
  // Test Case: Hash Password
  it("should hash a password", async () => {
    const password = "testpassword";
    const hashedPassword = await passwordUtils.hashPassword(password);
    expect(hashedPassword).toBeDefined();
    expect(typeof hashedPassword).toBe("string");
  });

  // Test Case: Compare Password
  it("should compare a valid password", async () => {
    const password = "testpassword";
    const hashedPassword = await passwordUtils.hashPassword(password);
    const isPasswordValid = await passwordUtils.comparePassword(
      password,
      hashedPassword
    );
    expect(isPasswordValid).toBe(true);
  });

  // Test Case: Compare Invalid Password
  it("should compare an invalid password", async () => {
    const password = "testpassword";
    const hashedPassword = await passwordUtils.hashPassword(password);
    const isPasswordValid = await passwordUtils.comparePassword(
      "wrongpassword",
      hashedPassword
    );
    expect(isPasswordValid).toBe(false);
  });
});

Enter fullscreen mode Exit fullscreen mode

and src/__tests__/utils/jwtUtils.test.ts and place the following code:

import {
  JWTPayload,
  generateToken,
  verifyToken,
  verifyTokenAndAuthorization,
} from "../../utils/jwtUtils";

import dotenv from "dotenv";
dotenv.config();

describe("JWT Utils Tests", () => {
  const mockPayload: JWTPayload = {
    id: "mockUserId",
    username: "mockUsername",
    email: "mock@example.com",
    isAdmin: false,
  };

  const mockToken = "mockToken";

  process.env.JWT_SEC = "mockSecret";
  process.env.JWT_EXPIRY_PERIOD = "1h";

  // Test Case: Generate Token
  it("should generate a JWT token", () => {
    const token = generateToken(mockPayload);
    expect(token).toBeDefined();
  });


  it("should verify a valid JWT token", (done) => {
    const req: any = {
      headers: {
        token: `Bearer ${mockToken}`,
      },
    };

    const res: any = {
      status: (status: number) => {
        expect(status).toBe(403);
        return {
          json: (message: string) => {
            expect(message).toBe("Token is not valid!");
            done();
          },
        };
      },
    };

    const next = () => {
      // Should not reach here
      done.fail("Should not reach next middleware on invalid token");
    };

    verifyToken(req, res, next);
  });

  // Test Case: Verify Token (invalid token)
  it("should handle an invalid JWT token", (done) => {
    const req: any = {
      headers: {
        token: "InvalidToken",
      },
    };

    const res: any = {
      status: (status: number) => {
        expect(status).toBe(403);
        return {
          json: (message: string) => {
            expect(message).toBe("Token is not valid!");
            done();
          },
        };
      },
    };

    const next = () => {
      // Should not reach here
      done.fail("Should not reach next middleware on invalid token");
    };

    verifyToken(req, res, next);
  });

  // Cleanup: Reset environment variables after tests
  afterAll(() => {
    delete process.env.JWT_SEC;
    delete process.env.JWT_EXPIRY_PERIOD;
  });
});

Enter fullscreen mode Exit fullscreen mode

In our suite for hashing the password, we examine the core functionalities integral to safeguarding sensitive credentials. The initial litmus test involves the seamless hashing of passwords, ensuring a robust defense against unauthorized access. A keen eye is cast upon the compare password mechanism, where the suite deftly validates the correctness of a given password against its hashed counterpart. In cases of both valid and invalid password comparisons, the suite orchestrates a meticulous evaluation, fortifying our application's resilience against potential security threats. This comprehensive scrutiny instills confidence in the reliability and effectiveness of our Password Utilities, as they stand guard to fortify the fortress of our user authentication system.

Also, in the suite for token generation and verification, we scrutinize the fundamental functionalities that underpin the secure handling of JSON Web Tokens (JWTs). The first checkpoint ensures the seamless generation of JWT tokens, a cornerstone in our approach to secure authentication. Delving into token verification, the suite rigorously examines scenarios of both valid and invalid tokens, intricately assessing the resilience of our verification mechanism. In cases where a valid token is presented, the verification process is expected to seamlessly proceed, ensuring that the user details are defined. Conversely, when confronted with an invalid token, our suite orchestrates a precise response, safeguarding our application against unauthorized access. As the final act of diligence, the suite gracefully resets environment variables, leaving the testing landscape pristine.

In the User Service Tests suite, we meticulously verify the functionality of our user service within a Node.js backend. Leveraging the power of mocking, we intentionally isolate the service from external dependencies, ensuring a controlled testing environment. Our suite encompasses crucial aspects, such as creating a new user, logging in a user, retrieving all users, and deleting an existing user. Each test case meticulously orchestrates mock data and expectations, guaranteeing the service operates seamlessly. Our commitment to best practices shines through as we meticulously clean up by removing the created user post-testing. To facilitate more accurate comparisons of user objects, we employ the removeMongoProps function, eliminating MongoDB-specific properties. This suite is a testament to our dedication to thorough and isolated testing, laying a robust foundation for a resilient backend service.

On the otherhand, in the Product Service Tests suite, we scrutinize the functionality of our backend API's product-related operations. Employing the power of mocking, we deliberately isolate the service from external dependencies, ensuring a controlled testing environment. The suite comprehensively tests key functionalities, including creating a new product, fetching a product by ID, updating a product, deleting a product, and fetching all products. Each test case is meticulously crafted, incorporating mock data, precise expectations, and detailed property comparisons, thereby validating the robustness and reliability of our ProductService. The suite adheres to best practices, ensuring that the testing environment is thoroughly cleaned up post-execution by closing the MongoDB connection and clearing all Jest mocks. This comprehensive and systematic testing approach reinforces our commitment to delivering a resilient and dependable backend service.

Writing Unit Tests for Controllers

Create a new file: src/__tests__/controllers/userController.test.ts and place the following code:

import { Request, Response } from "express";
import * as UserController from "../../controllers/userController";
import * as UserService from "../../services/userService";
import { IProduct } from "models/Product";
import { IUser } from "models/User";

// Mock the UserService
jest.mock("../../services/userService");

describe("User Controller Tests", () => {
  // Test Case: Create a new user
  it("should create a new user", async () => {
    // Mock user data from the request body
    const mockUserData: IUser = {
      email: "test@example.com",
      username: "testuser",
      password: "testpassword",
      isAdmin: false,
      savedProducts: [],
    } as IUser;

    // Mock the create user response from the UserService
    const mockCreatedUser = {
      _id: "mockUserId",
      ...mockUserData,
    };

    // Mock the request and response objects
    const mockRequest = {
      body: mockUserData,
    } as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the createUser function of the UserService
    (UserService.createUser as jest.Mock).mockResolvedValueOnce(
      mockCreatedUser
    );

    // Call the createUser controller
    await UserController.createUser(mockRequest, mockResponse);

    // Expectations
    expect(UserService.createUser).toHaveBeenCalledWith(mockUserData);
    expect(mockResponse.status).toHaveBeenCalledWith(201);
    expect(mockResponse.json).toHaveBeenCalledWith(mockCreatedUser);
  }, 20000);

  // Test Case: Login user
  it("should login a user and return a token", async () => {
    // Mock user credentials from the request body
    const mockUserCredentials = {
      email: "test@example.com",
      password: "testpassword",
    };

    type IMockCreatedUser = {
      user: Partial<IUser>;
      token: string;
    };
    // Mock the login response from the UserService
    const mockLoginResponse: IMockCreatedUser = {
      user: {
        _id: "mockUserId",
        email: mockUserCredentials.email,
        username: "testuser",
        isAdmin: false,
        savedProducts: [],
      },
      token: "mocked_token",
    };

    // Mock the request and response objects
    const mockRequest = {
      body: mockUserCredentials,
    } as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the loginUser function of the UserService
    (UserService.loginUser as jest.Mock).mockResolvedValueOnce(
      mockLoginResponse
    );

    // Call the loginUser controller
    await UserController.loginUser(mockRequest, mockResponse);

    // Expectations
    expect(UserService.loginUser).toHaveBeenCalledWith(
      mockUserCredentials.email,
      mockUserCredentials.password
    );
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith(mockLoginResponse);
  }, 20000);

  // Test Case: Get all users
  it("should get all users", async () => {
    // Mock the array of users from the UserService
    const mockUsers: IUser[] = [
      {
        _id: "mockUserId1",
        email: "user1@example.com",
        username: "user1",
        isAdmin: false,
        savedProducts: [],
      },
      {
        _id: "mockUserId2",
        email: "user2@example.com",
        username: "user2",
        isAdmin: true,
        savedProducts: [],
      },
    ] as IUser[];

    // Mock the request and response objects
    const mockRequest = {} as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getAllUsers function of the UserService
    (UserService.getAllUsers as jest.Mock).mockResolvedValueOnce(mockUsers);

    // Call the getAllUsers controller
    await UserController.getAllUsers(mockRequest, mockResponse);

    // Expectations
    expect(UserService.getAllUsers).toHaveBeenCalled();
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith(mockUsers);
  }, 20000);

  // Test Case: Error fetching users
  it("should handle error when fetching users", async () => {
    // Mock the error response from the UserService
    const mockErrorResponse = {
      message: "Error fetching users",
    };

    // Mock the request and response objects
    const mockRequest = {} as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getAllUsers function of the UserService to throw an error
    (UserService.getAllUsers as jest.Mock).mockRejectedValueOnce(
      mockErrorResponse
    );

    // Call the getAllUsers controller
    await UserController.getAllUsers(mockRequest, mockResponse);

    // Expectations
    expect(UserService.getAllUsers).toHaveBeenCalled();
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: mockErrorResponse.message,
    });
  }, 20000);

  // Test Case: Get user by ID
  it("should get user by ID", async () => {
    // Mock the user from the UserService
    const mockUser: IUser = {
      _id: "mockUserId",
      email: "mock@example.com",
      username: "mockUser",
      isAdmin: false,
      savedProducts: [],
    } as IUser;

    // Mock the request and response objects
    const mockRequest: any = {
      params: {
        userId: "mockUserId",
      },
    };

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getUserById function of the UserService
    (UserService.getUserById as jest.Mock).mockResolvedValueOnce(mockUser);

    // Call the getUserById controller
    await UserController.getUserById(mockRequest, mockResponse);

    // Expectations
    expect(UserService.getUserById).toHaveBeenCalledWith(
      mockRequest.params.userId
    );
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith(mockUser);
  }, 20000);

  // Test Case: User not found
  it("should handle case where user is not found", async () => {
    // Mock the request and response objects
    const mockRequest: any = {
      params: {
        userId: "nonexistentUserId",
      },
    };

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getUserById function of the UserService to return null
    (UserService.getUserById as jest.Mock).mockResolvedValueOnce(null);

    // Call the getUserById controller
    await UserController.getUserById(mockRequest, mockResponse);

    // Expectations
    expect(UserService.getUserById).toHaveBeenCalledWith(
      mockRequest.params.userId
    );
    expect(mockResponse.status).toHaveBeenCalledWith(404);
    expect(mockResponse.json).toHaveBeenCalledWith({ error: "User not found" });
  }, 20000);

  // Test Case: Error fetching user
  it("should handle error when fetching user by ID", async () => {
    // Mock the error response from the UserService
    const mockErrorResponse = {
      message: "Error fetching user",
    };

    // Mock the request and response objects
    const mockRequest: any = {
      params: {
        userId: "errorUserId",
      },
    };

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getUserById function of the UserService to throw an error
    (UserService.getUserById as jest.Mock).mockRejectedValueOnce(
      mockErrorResponse
    );

    // Call the getUserById controller
    await UserController.getUserById(mockRequest, mockResponse);

    // Expectations
    expect(UserService.getUserById).toHaveBeenCalledWith(
      mockRequest.params.userId
    );
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: mockErrorResponse.message,
    });
  }, 20000);

  // Test Case: Delete user by ID
  it("should delete user by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request<
      { userId: string },
      any,
      any,
      any,
      Record<string, any>
    > = {
      params: {
        userId: "mockUserId",
      },
    } as Request<{ userId: string }, any, any, any, Record<string, any>>;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the deleteUser function of the UserService
    (UserService.deleteUser as jest.Mock).mockResolvedValueOnce(null);

    // Call the deleteUser controller
    await UserController.deleteUser(mockRequest, mockResponse);

    // Expectations
    expect(UserService.deleteUser).toHaveBeenCalledWith(
      mockRequest.params.userId
    );
    expect(mockResponse.status).toHaveBeenCalledWith(204);
    expect(mockResponse.send).toHaveBeenCalled();
    expect(mockResponse.json).not.toHaveBeenCalled(); // Ensure json is not called for a 204 status
  }, 20000);
});

Enter fullscreen mode Exit fullscreen mode

and src/__tests__/controllers/productController.test.ts and place the following code:

import { Request, Response } from "express";
import * as ProductController from "../../controllers/productController";
import * as ProductService from "../../services/productService";

// Mock the ProductService
jest.mock("../../services/productService");

describe("Product Controller Tests", () => {
  // Test Case: Create a new product
  it("should create a new product", async () => {
    // Mock the request and response objects
    const mockRequest: Request<{}, any, any, any, Record<string, any>> = {
      body: {
        title: "Mocked Product",
        description: "A description for the mocked product",
        image: "mocked_image.jpg",
        category: "Mocked Category",
        quantity: "10",
        inStock: true,
      },
    } as Request<{}, any, any, any, Record<string, any>>;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the createProduct function of the ProductService
    (ProductService.createProduct as jest.Mock).mockResolvedValueOnce({
      _id: "mockedProductId",
      title: "Mocked Product",
      description: "A description for the mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    });

    // Call the createProduct controller
    await ProductController.createProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.createProduct).toHaveBeenCalledWith(mockRequest.body);
    expect(mockResponse.status).toHaveBeenCalledWith(201);
    expect(mockResponse.json).toHaveBeenCalledWith({
      _id: "mockedProductId",
      title: "Mocked Product",
      description: "A description for the mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 201 status
  });

  // Test Case: Get all products - Success
  it("should get all products successfully", async () => {
    // Mock the request and response objects
    const mockRequest: any = {};

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getAllProducts function of the ProductService
    (ProductService.getAllProducts as jest.Mock).mockResolvedValueOnce([
      {
        _id: "mockedProductId1",
        title: "Mocked Product 1",
        description: "Description for mocked product 1",
        image: "image1.jpg",
        category: "Category 1",
        quantity: "5",
        inStock: true,
      },
      {
        _id: "mockedProductId2",
        title: "Mocked Product 2",
        description: "Description for mocked product 2",
        image: "image2.jpg",
        category: "Category 2",
        quantity: "10",
        inStock: false,
      },
    ]);

    // Call the getAllProducts controller
    await ProductController.getAllProducts(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.getAllProducts).toHaveBeenCalled();
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith([
      {
        _id: "mockedProductId1",
        title: "Mocked Product 1",
        description: "Description for mocked product 1",
        image: "image1.jpg",
        category: "Category 1",
        quantity: "5",
        inStock: true,
      },
      {
        _id: "mockedProductId2",
        title: "Mocked Product 2",
        description: "Description for mocked product 2",
        image: "image2.jpg",
        category: "Category 2",
        quantity: "10",
        inStock: false,
      },
    ]);
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 200 status
  });

  // Test Case: Get all products - Error
  it("should handle errors when getting all products", async () => {
    // Mock the request and response objects
    const mockRequest: any = {};

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getAllProducts function of the ProductService to throw an error
    (ProductService.getAllProducts as jest.Mock).mockRejectedValueOnce(
      new Error("Error getting products")
    );

    // Call the getAllProducts controller
    await ProductController.getAllProducts(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.getAllProducts).toHaveBeenCalled();
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Error getting products",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 500 status
  });

  // Test Case: Get product by ID - Success
  it("should get product by ID successfully", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getProductById function of the ProductService
    (ProductService.getProductById as jest.Mock).mockResolvedValueOnce({
      _id: "mockedProductId",
      title: "Mocked Product",
      description: "Description for mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    });

    // Call the getProductById controller
    await ProductController.getProductById(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.getProductById).toHaveBeenCalledWith(
      "mockedProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith({
      _id: "mockedProductId",
      title: "Mocked Product",
      description: "Description for mocked product",
      image: "mocked_image.jpg",
      category: "Mocked Category",
      quantity: "10",
      inStock: true,
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 200 status
  });

  // Test Case: Get product by ID - Not Found
  it("should handle product not found when getting by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "nonExistentProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getProductById function of the ProductService to return null
    (ProductService.getProductById as jest.Mock).mockResolvedValueOnce(null);

    // Call the getProductById controller
    await ProductController.getProductById(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.getProductById).toHaveBeenCalledWith(
      "nonExistentProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(404);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Product not found",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 404 status
  });

  // Test Case: Get product by ID - Error
  it("should handle errors when getting product by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the getProductById function of the ProductService to throw an error
    (ProductService.getProductById as jest.Mock).mockRejectedValueOnce(
      new Error("Error getting product by ID")
    );

    // Call the getProductById controller
    await ProductController.getProductById(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.getProductById).toHaveBeenCalledWith(
      "mockedProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Error getting product by ID",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 500 status
  });

  // Test Case: Update product by ID - Success
  it("should update product by ID successfully", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
      body: {
        title: "Updated Product",
        description: "Updated description",
        image: "updated_image.jpg",
        category: "Updated Category",
        quantity: "15",
        inStock: false,
      },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the updateProduct function of the ProductService
    (ProductService.updateProduct as jest.Mock).mockResolvedValueOnce({
      _id: "mockedProductId",
      title: "Updated Product",
      description: "Updated description",
      image: "updated_image.jpg",
      category: "Updated Category",
      quantity: "15",
      inStock: false,
    });

    // Call the updateProduct controller
    await ProductController.updateProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.updateProduct).toHaveBeenCalledWith(
      "mockedProductId",
      mockRequest.body
    );
    expect(mockResponse.status).toHaveBeenCalledWith(200);
    expect(mockResponse.json).toHaveBeenCalledWith({
      _id: "mockedProductId",
      title: "Updated Product",
      description: "Updated description",
      image: "updated_image.jpg",
      category: "Updated Category",
      quantity: "15",
      inStock: false,
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 200 status
  });

  // Test Case: Update product by ID - Not Found
  it("should handle product not found when updating by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "nonExistentProductId" },
      body: {
        title: "Updated Product",
        description: "Updated description",
        image: "updated_image.jpg",
        category: "Updated Category",
        quantity: "15",
        inStock: false,
      },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the updateProduct function of the ProductService to return null
    (ProductService.updateProduct as jest.Mock).mockResolvedValueOnce(null);

    // Call the updateProduct controller
    await ProductController.updateProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.updateProduct).toHaveBeenCalledWith(
      "nonExistentProductId",
      mockRequest.body
    );
    expect(mockResponse.status).toHaveBeenCalledWith(404);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Product not found",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 404 status
  });

  // Test Case: Update product by ID - Error
  it("should handle errors when updating product by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
      body: {
        title: "Updated Product",
        description: "Updated description",
        image: "updated_image.jpg",
        category: "Updated Category",
        quantity: "15",
        inStock: false,
      },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the updateProduct function of the ProductService to throw an error
    (ProductService.updateProduct as jest.Mock).mockRejectedValueOnce(
      new Error("Error updating product by ID")
    );

    // Call the updateProduct controller
    await ProductController.updateProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.updateProduct).toHaveBeenCalledWith(
      "mockedProductId",
      mockRequest.body
    );
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Error updating product by ID",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 500 status
  });

  // Test Case: Delete product by ID - Success
  it("should delete product by ID successfully", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
    } as unknown as Response;

    // Call the deleteProduct function of the ProductService
    await ProductController.deleteProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.deleteProduct).toHaveBeenCalledWith(
      "mockedProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(204);
    expect(mockResponse.send).toHaveBeenCalled();
  });

  // Test Case: Delete product by ID - Not Found
  it("should handle product not found when deleting by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "nonExistentProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the deleteProduct function of the ProductService to return null
    (ProductService.deleteProduct as jest.Mock).mockResolvedValueOnce(null);

    // Call the deleteProduct controller
    await ProductController.deleteProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.deleteProduct).toHaveBeenCalledWith(
      "nonExistentProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(204);
    expect(mockResponse.send).toHaveBeenCalled();
    expect(mockResponse.json).not.toHaveBeenCalled(); // Ensure json is not called for a 204 status
  });

  // Test Case: Delete product by ID - Error
  it("should handle errors when deleting product by ID", async () => {
    // Mock the request and response objects
    const mockRequest: Request = {
      params: { productId: "mockedProductId" },
    } as unknown as Request;

    const mockResponse = {
      status: jest.fn().mockReturnThis(),
      send: jest.fn(),
      json: jest.fn(),
    } as unknown as Response;

    // Mock the deleteProduct function of the ProductService to throw an error
    (ProductService.deleteProduct as jest.Mock).mockRejectedValueOnce(
      new Error("Error deleting product by ID")
    );

    // Call the deleteProduct controller
    await ProductController.deleteProduct(mockRequest, mockResponse);

    // Expectations
    expect(ProductService.deleteProduct).toHaveBeenCalledWith(
      "mockedProductId"
    );
    expect(mockResponse.status).toHaveBeenCalledWith(500);
    expect(mockResponse.json).toHaveBeenCalledWith({
      error: "Error deleting product by ID",
    });
    expect(mockResponse.send).not.toHaveBeenCalled(); // Ensure send is not called for a 500 status
  });
});
Enter fullscreen mode Exit fullscreen mode

In the test suite for the User Controller, we employ thorough mocking using jest.mock to isolate the UserService, ensuring controlled testing of the controller's functionalities. Each test case is meticulously crafted to cover key aspects of user management, such as creating a new user, user login, fetching all users, retrieving a user by ID, handling scenarios where the user is not found, and deleting a user by ID. The suite demonstrates the usage of precise mock data and expectations, validating the User Controller's reliability. Notably, the jest.mock line is crucial, replacing the actual UserService with a mock implementation to create a controlled environment for testing. This approach ensures that the tests focus solely on the User Controller's behavior without interference from external dependencies. This testing strategy aims to deliver a secure, efficient, and error-resilient user management system in our backend API.

In the Product Controller Tests test suite for the Product Controller, we orchestrate a comprehensive set of scenarios to validate the functionality of the controller methods. The tests cover creating a new product, fetching all products (both successful and error cases), retrieving a product by ID (with successful, not found, and error scenarios), updating a product by ID (again, covering success, not found, and error cases), and finally, deleting a product by ID (encompassing successful, not found, and error scenarios). Each test is structured with meticulous mock data, expectations, and error handling, ensuring a robust examination of the Product Controller's behavior. The jest.mock("../../services/productService") helps us to isolate the ProductService for controlled testing.
All set for our controllers, lets also discuss how we write tests for our routes.

Writing Unit Tests for Routes

Before we start to write our unit tests for all our routes, we need to first examine our setup and make some adjustments.
First we need to isolate a testing server to avoid conflicts testing various suites for different routes.
Create a new file in the root directory, call it: test_setup.ts and place the following code:

import app from "./src/index"; // Import your main app from the index file

const port = 9000; // Choose a suitable port

const test_server = app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

export default test_server;

Enter fullscreen mode Exit fullscreen mode

This helps us interact with the testing server instead of using the actual server of ours running at port 8800.
Lets also add the following line to our .env file:

TEST_ENV=true
Enter fullscreen mode Exit fullscreen mode

and start our main server conditionally in our src/index.ts according to TEST_ENV environment variable.
Replace:

 app.listen(PORT, () => {
    console.log(`Backend server is running at port ${PORT}`);
  });
Enter fullscreen mode Exit fullscreen mode

with

// Check if it's not a test environment before starting the server
if (!process.env.TEST_ENV) {
  app.listen(PORT, () => {
    console.log(`Backend server is running at port ${PORT}`);
  });
}
Enter fullscreen mode Exit fullscreen mode

to start our main server only if we are not testing mode.
Don't forget to alter the TEST_ENV by setting it to false when youre done running unit tests or when you get in production.

Create a new file: src/__tests__/routes/userRoutes.test.ts and place the following code:

// userRoutes.test.ts
import request from "supertest";
import test_server from "../../../test_setup";

import User from "../../models/User";
import supertest from "supertest";

// You can either test with a real token like this:
const adminToken = "YourAdminToken";
// Alternatively:
const nonAdminToken = "YourNonAdminToken";
// OR
// Write a function that simulates an actual login process and extract the token from there
// And then store it in a variable for use in various test cases e.g:
// Assuming you have admin credentials for testing

// const adminCredentials = {
//   email: "admin@example.com",
//   password: "adminpassword",
// };

// Assuming you have a function to generate an authentication token
// const getAuthToken = async () => {
//   const response = await supertest(test_server)
//     .post("/api/v1/users/login")
//     .send(adminCredentials);
//   return response.body.token;
// };

// Get the admin authentication token (inside an async function or code block)
// const authToken = await getAuthToken();

// ------------------> do the same for non admin user and store the token in
// its own variable

describe("User Routes", () => {
  beforeAll(async () => {});

  // Clean up after tests
  afterAll(async () => {
    // Remove the created user
    await User.deleteOne({
      username: "testuser",
    });

    // Clear all jest mocks
    jest.clearAllMocks();
    test_server.close();
  });

  // Test case for creating a new user
  it("should create a new user", async () => {
    const response = await request(test_server)
      .post("/api/v1/users/create") // Update the route path accordingly
      .send({
        // Your user data for testing
        email: "admin@example.com",
        password: "adminpassword",
        username: "testuser",
        isAdmin: false,
        savedProducts: [],
      });

    // Expectations
    expect(response.status).toBe(201);
    expect(response.body).toHaveProperty("email", "admin@example.com");
    expect(response.body).toHaveProperty("username", "testuser");
    // Add more expectations based on your user data

    // Optionally, you can store the created user for future tests
    const createdUser = response.body;
  }, 20000);

  // Test case for logging in a user
  it("should login a user and return a token", async () => {
    // Assuming you have a test user created previously
    const testUser = {
      email: "admin@example.com",
      password: "adminpassword",
    };

    const response = await request(test_server)
      .post("/api/v1/users/login") // Update the route path accordingly
      .send({
        email: testUser.email,
        password: testUser.password,
      });

    // Expectations
    expect(response.status).toBe(200);
    expect(response.body).toHaveProperty("user");
    expect(response.body).toHaveProperty("token");
    // Add more expectations based on your login data

    // Optionally, you can store the token for future authenticated requests
    const authToken = response.body.token;
  }, 20000);

  // Assuming you have admin credentials for testing
  const adminCredentials = {
    email: "admin@example.com",
    password: "adminpassword",
  };

  // Assuming you have a function to generate an authentication token
  const getAuthToken = async (credentials: object) => {
    const response = await supertest(test_server)
      .post("/api/v1/users/login")
      .send(credentials);
    return response.body.token;
  };

  // Test case for getting all users (admin access)
  it("should get all users with admin access", async () => {
    // Assuming you have admin credentials for testing
    const adminCredentials = {
      email: "admin@example.com",
      password: "adminpassword",
    };

    // Log in as admin to get the admin token
    // const adminToken = await getAuthToken(adminCredentials);

    // Send a request to the route with the admin token
    const response = await request(test_server)
      .get("/api/v1/users/all")
      .set("token", `Bearer ${adminToken}`);

    // Expectations
    expect(response.status).toBe(200);
    // Add more expectations based on your user data

    // Optionally, you can store the users for further assertions
    const allUsers = response.body;
  }, 20000);

  // Test case for getting all users without admin access
  it("should return 403 Forbidden when accessing all users without admin access", async () => {
    // Assuming you have non-admin credentials for testing
    const nonAdminCredentials = {
      email: "nonadmin@example.com",
      password: "nonadminpassword",
    };

    // Log in as non-admin to get the non-admin token
    // const nonAdminToken = await getAuthToken(nonAdminCredentials);

    // Send a request to the route with the non-admin token
    const response = await request(test_server)
      .get("/api/v1/users/all")
      .set("token", `Bearer ${nonAdminToken}`);

    // Expectations
    expect(response.status).toBe(403);
    // Add more expectations based on how you handle non-admin access
  }, 20000);

  // Test case: Should successfully update a user with valid credentials (admin or account owner)
  it("should successfully update a user with valid credentials (admin or account owner)", async () => {
    // Assuming you have a test user created previously
    // If you dont have you can create one programattically like we did writing
    // Tests for create new user

    const testUser = {
      email: "testuser@example.com",
      password: "testuserpassword",
      username: "testuser",
    };

    // Update the user using the hardcoded token
    const updateResponse = await request(test_server)
      .put(`/api/v1/users/update/6593ca275db905747ea085aa`)
      .set("token", `Bearer ${adminToken}`)
      .send({
        // Your updated user data
        username: "updateduser",
      });

    // Expectations
    expect(updateResponse.status).toBe(200);
    expect(updateResponse.body).toHaveProperty("username", "updateduser");
    // Add more expectations based on your updated user data
  }, 20000);

  // Test case: Should return 403 Forbidden when updating a user without valid credentials
  it("should return 403 Forbidden when updating a user without valid credentials", async () => {
    // Assuming you have a different user for testing
    const otherUserCredentials = {
      email: "otheruser@example.com",
      password: "otheruserpassword",
      username: "otheruser",
    };

    // Create a different user
    const createResponse = await request(test_server)
      .post("/api/v1/users/create")
      .send(otherUserCredentials);

    // Get the ID of the created user
    const otherUserId = createResponse.body._id;

    // Attempt to update the user without valid credentials (not admin or account owner)
    const updateResponse = await request(test_server)
      .put(`/api/v1/users/update/${otherUserId}`)
      .set("token", `Bearer ${nonAdminToken}`)
      .send({
        // Your updated user data
        username: "updateduser",
      });

    // Expectations
    expect(updateResponse.status).toBe(403);
    // Add more expectations based on how you handle non-authorized updates
  }, 20000);

  // Test case: Should successfully delete a user with valid credentials (admin or account owner)
  it("should successfully delete a user with valid credentials (admin or account owner)", async () => {
    // Assuming you have a test user created previously
    const testUser = {
      email: "testuser@example.com",
      password: "testuserpassword",
      username: "testuser",
    };

    // Create a test user
    const createResponse = await request(test_server)
      .post("/api/v1/users/create")
      .send(testUser);

    // Get the ID of the created user
    const userId = createResponse.body._id;

    // Log the ID to check if it's correct
    console.log("User ID:", userId);

    // Delete the user using the hardcoded token
    const deleteResponse = await request(test_server)
      .delete(`/api/v1/users/delete/${userId}`)
      .set("token", `Bearer ${adminToken}`);

    // Expectations
    expect(deleteResponse.status).toBe(204);
  }, 20000);
});
Enter fullscreen mode Exit fullscreen mode

and another file:
src/__tests__/routes/productRoutes.test.ts and place the following code:

import request from "supertest";
import test_server from "../../../test_setup";
import supertest from "supertest";

// You can either test with a real token like this:
const adminToken = "YourAdminToken";
// Alternatively:
const nonAdminToken = "YourNonAdminToken";

// OR
// Write a function that simulates an actual login process and extract the token from there
// And then store it in a variable for use in various test cases e.g:
// Assuming you have admin credentials for testing

// const adminCredentials = {
//   email: "admin@example.com",
//   password: "adminpassword",
// };

// Assuming you have a function to generate an authentication token
// const getAuthToken = async () => {
//   const response = await supertest(test_server)
//     .post("/api/v1/users/login")
//     .send(adminCredentials);
//   return response.body.token;
// };

// Get the admin authentication token (inside an async function or code block)
// const authToken = await getAuthToken();

// ------------------> do the same for non admin user and store the token in
// its own variable

// Assuming you have a test product data
const testProduct = {
  title: "Test Product",
  description: "This is a test product",
  image: "test-image.jpg",
  category: "Test Category",
  quantity: "10",
  inStock: true,
};
describe("User Routes", () => {
  // Test case: Should successfully create a new product with valid admin credentials
  it("should successfully create a new product with valid admin credentials", async () => {
    // Create a new product using the admin token
    const createResponse = await request(test_server)
      .post("/api/v1/products/create")
      .set("token", `Bearer ${adminToken}`)
      .send(testProduct);

    // Expectations
    expect(createResponse.status).toBe(201);
    expect(createResponse.body).toHaveProperty("title", "Test Product");
    // Add more expectations based on your product data
  }, 20000);

  // Test case: Should successfully get all products without authentication
  it("should successfully get all products without authentication", async () => {
    // Make a request to the endpoint without providing an authentication token
    const response = await request(test_server).get("/api/v1/products/all");

    // Expectations
    expect(response.status).toBe(200);
    // Add more expectations based on your implementation
  }, 20000);

  // Test case: Should successfully get a product by ID with a valid token
  it("should successfully get a product by ID with a valid token", async () => {
    // Assuming you have a product ID for testing
    const productId = "6593e048731070abb0939faf";

    // Make a request to the endpoint with a valid token and product ID
    const response = await request(test_server)
      .get(`/api/v1/products/${productId}`)
      .set("token", `Bearer ${adminToken}`);

    // Expectations
    expect(response.status).toBe(200);
    // Add more expectations based on your implementation
  }, 20000);

  // Test case: Should successfully update a product by ID with admin access
  it("should successfully update a product by ID with admin access", async () => {
    // Assuming you have a product ID for testing
    const productId = "6593e048731070abb0939faf";

    // Replace 'yourUpdatedProductData' with the data you want to update the product with
    const updatedProductData = {
      title: "Updated Product",
      description: "Updated Product Description",
      // Add more fields as needed
    };

    // Make a request to the endpoint with an admin token and product ID
    const response = await request(test_server)
      .put(`/api/v1/products/update/${productId}`)
      .set("token", `Bearer ${adminToken}`)
      .send(updatedProductData);

    // Expectations
    expect(response.status).toBe(200);
    // Add more expectations based on your implementation
  }, 20000);

  // Test case: Should successfully delete a product by ID with admin access
  it("should successfully delete a product by ID with admin access", async () => {
    // Assuming you have a product ID for testing
    const productId = "6593e0a5451f5e47ce363e00";

    // Make a request to the endpoint with an admin token and product ID
    const response = await request(test_server)
      .delete(`/api/v1/products/delete/${productId}`)
      .set("token", `Bearer ${adminToken}`);

    // Expectations
    expect(response.status).toBe(204);
    // Add more expectations based on your implementation
  }, 20000);
});

afterAll(() => {
  test_server.close();
});

Enter fullscreen mode Exit fullscreen mode

In our routes files like src/routes/userRoutes.ts, our routes donot include the prefix /api/v1 as its only included in the src/index.ts file where the src/routes/index.ts is imported and included. However,dont forget to add it while writing unit tests for all the routes as they will all return a 404 status code in its absence.

Our tests involve usage of our utility functions, JWT middlewares in src/utils/jwtUtils.ts so lets first complete their unit tests in a separate file: src/__tests__/utils/jwtAuth.test.ts with the following code:

import {
  verifyTokenAndAdmin,
  verifyTokenAndAuthorization,
} from "../../utils/jwtUtils";
import dotenv from "dotenv";
dotenv.config();

// Mock token for testing purposes
const mockTokenForNonAdmin = "YourMockTokenForNonAdmin";

const mockTokenForAdmin = "YourMockTokenForAdmin";
// You can also simulate an actual login process,
// extract the token and store it in a variable like this:

// const adminCredentials = {
//   email: "admin@example.com",
//   password: "adminpassword",
// };

// Assuming you have a function to generate an authentication token
// const getAuthToken = async () => {
//   const response = await supertest(test_server)
//     .post("/api/v1/users/login")
//     .send(adminCredentials);
//   return response.body.token;
// };

// Get the admin authentication token (inside an async function or code block)
// const authToken = await getAuthToken();

// Authorization Test Cases
describe("Authorization Tests", () => {
  // Test Case: Verify Token and Authorization (valid token and authorization)
  it("should verify a valid JWT token and authorization", (done) => {
    const req: any = {
      headers: {
        token: `Bearer ${mockTokenForNonAdmin}`,
      },
      user: {
        id: "mockUserId",
        isAdmin: false,
      },
      params: {
        id: "mockUserId",
      },
    };

    const res: any = {
      status: () => res, // Mock status function
      json: (message: string) => {
        // Assert the message or other expectations
        expect(req.user).toBeDefined();
        done();
      },
    };

    const next = () => {
      // Should not reach here
      done.fail("Should not reach next middleware on valid token");
    };

    verifyTokenAndAuthorization(req, res, next);
  });

  // Test Case: Verify Token and Authorization (unauthorized)
  it("should handle unauthorized access for authorization", (done) => {
    const req: any = {
      headers: {
        token: `Bearer ${mockTokenForNonAdmin}`,
      },
      user: {
        id: "otherUserId",
        isAdmin: false,
      },
      params: {
        id: "mockUserId",
      },
    };

    const res: any = {
      status: () => res, // Mock status function
      json: (message: string) => {
        // Assert the message or other expectations
        expect(message).toBe("You are not allowed to do that!");
        done();
      },
    };

    const next = () => {
      // Should not reach here
      done.fail("Should not reach next middleware on unauthorized access");
    };

    verifyTokenAndAuthorization(req, res, next);
  });
});

// Admin Access Test Cases
describe("Admin Access Tests", () => {
  // Test Case: Verify Token and Admin Access (valid token and admin)
  it("should verify a valid JWT token and admin access", (done) => {
    const req: any = {
      headers: {
        token: `Bearer ${mockTokenForNonAdmin}`,
      },
      user: {
        id: "mockUserId",
        isAdmin: true,
      },
      params: {
        id: "mockUserId",
      },
    };

    const res: any = {
      status: () => res, // Mock status function
      json: (message: string) => {
        // Assert the message or other expectations
        expect(req.user).toBeDefined();
        done();
      },
    };

    const next = () => {
      // Should not reach here
      done.fail(
        "Should not reach next middleware on valid token and admin access"
      );
    };

    verifyTokenAndAdmin(req, res, next);
  });

  // Test Case: Verify Token and Admin Access (non-admin)
  it("should handle non-admin access for admin authorization", (done) => {
    const req: any = {
      headers: {
        token: `Bearer ${mockTokenForNonAdmin}`,
      },
      user: {
        id: "mockUserId",
        isAdmin: false,
      },
      params: {
        id: "mockUserId",
      },
    };

    const res: any = {
      status: () => res, // Mock status function
      json: (message: string) => {
        // Assert the message or other expectations
        expect(message).toBe("You are not allowed to do that!");
        done();
      },
    };

    const next = () => {
      // Should not reach here
      done.fail("Should not reach next middleware on non-admin access");
    };

    verifyTokenAndAdmin(req, res, next);
  });
});

Enter fullscreen mode Exit fullscreen mode

This suite has robust test cases to verify the functionality of two crucial middleware functions, verifyTokenAndAuthorization and verifyTokenAndAdmin, and is an intergral part of our previous tests written in src/__tests__/utils/jwtUtils.test.ts.

In the "Authorization Tests" section, we meticulously validate the proper functioning of token validation and authorization. We take pride in ensuring that our application accurately handles scenarios where a valid JWT token is presented along with appropriate authorization, as well as scenarios where unauthorized access is attempted, with clear error messages to guide the user.

Moving on to the "Admin Access Tests" section, we continue to demonstrate our dedication to a secure user access control system. Our tests encompass scenarios where valid tokens with admin privileges are successfully processed, and where non-admin users encounter appropriate error messages when attempting admin-protected actions.

By employing mocked request (req), response (res), and the next function, we simulate the middleware execution flow during testing. The use of the done callback underscores our commitment to thorough and reliable asynchronous testing methodologies.

In essence, our test suite for these middleware functions serves as a testament to our unwavering commitment to delivering a secure and robust Express.js application, ensuring that token verification, authorization, and admin access control are implemented with the utmost precision and reliability.

In our User Routes test suite, we ensure the robust functionality of various endpoints within our Express.js application. Leveraging the Supertest library, we validate the creation, login, retrieval, update, and deletion of user data. By employing both admin and non-admin tokens, we meticulously simulate scenarios that cover a spectrum of user access levels. Our tests underscore the meticulousness with which we handle user authentication, authorization, and access control, providing a solid foundation for the secure operation of our application. Furthermore, we prioritize cleanliness by incorporating cleanup procedures, inside afterAll method, to remove test users after execution, demonstrating our commitment to maintaining a pristine testing environment. With these tests, we confidently assure the reliability and security of our User Routes, fostering a robust user management system within our application.

On the otherhand, our Product Routes suite orchestrates a meticulous examination of various product endpoints to guarantee the seamless operation of our Express.js application. The tests encompass the creation, retrieval, update, and deletion of products, ensuring their secure management. The initiation of a new product with valid admin credentials is rigorously validated, emphasizing our commitment to robust functionality. Furthermore, we ensure that product retrieval, whether by ID or not, transpires smoothly, even in the absence of authentication. For administrative tasks like updating and deleting products, our suite validates the integrity of these operations with precision. Each test is meticulously crafted to assess the application's response under different scenarios, solidifying our confidence in the resilience and reliability of our Product Routes. Following the suite's execution, we gracefully close the server, maintaining an orderly testing environment.

By now you should have an output in the terminal with this ending:
Image description
Indicating that all the tests succeeded.
The Snapshots: 0 total message indicates that no Jest snapshots were created or updated during the test run. Snapshots in Jest are a way to automatically capture the output of a component or data structure and compare it against future changes to detect unintended changes.
Jest snapshots are often used in front-end testing, especially for UI components to capture the rendered output of components and help identify unintended changes. For our backend API, we were mostly dealing with server-side logic, database interactions, and APIs, snapshots were less critical which is why we dont have any.
Test Suites: 12 passed, 12 total mean we have 12 test suites and they all passed and Tests: 66 passed, 66 total means we have 66 tests and they all passed.
As you can see, opposed to rumours from most developers that unit testing is hard, its truly not hard given time but time consuming instead though worth it as previously discussed.

Overall Sammary

Embarking on a three-phase voyage, our article delved into the realm of Node.js and TypeScript development, sculpting a masterpiece of efficiency and reliability. In the inaugural phase, we meticulously established a robust environment, configuring the stage with precision using tools like Jest, Supertest among others. With the foundation laid, our journey traversed the expansive landscape of building a dynamic API with Express, navigating the intricacies of routing, controllers, and middleware. Finally, as our opus neared completion, we explored the symphony of unit testing, employing Jest and Supertest to ensure our code's fortitude. This trilogy encapsulates not just the process but the artistry behind crafting resilient, scalable, and well-tested Node.js applications in TypeScript, leaving developers equipped and inspired for their own epic odyssey in the world of software development.

Conclusion

In the symphony of Node.js and TypeScript development, orchestrating harmony through unit testing with Jest and Supertest, coupled with the formidable MongoDB and Mongoose ORM in the grandeur of Express, transforms the software development journey into an enlightened expedition. Through meticulous testing, we've navigated the intricacies of our codebase, ensuring resilience and reliability. With Jest as our virtuoso conductor, and Supertest as the keen-eared observer, the seamless integration of MongoDB and Mongoose has rendered our Express applications not just functional, but robust and secure. As we conclude this odyssey through the landscape of unit testing, we embrace a future where our code stands as a testament to its own quality, a melody composed with precision and played with confidence in the ever-evolving symphony of software development. May your code remain harmonious, your tests unwavering, and your development journey a continual crescendo of success.

Useful Links

Github Repo Twitter

Top comments (0)