DEV Community

Alfi Samudro Mulyo
Alfi Samudro Mulyo

Posted on • Updated on

Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 5

Scope of discussion:

  1. Explain about testing
  2. Create unit testing for users

So far, we've created two endpoints; users and posts.

Users endpoints:
POST /users/register,
POST /users/login,
GET /users/me,
PATCH /users/:id,
DELETE /users/:id.

Posts endpoints:
POST /posts,
GET /posts,
GET /posts/:id,
PATCH /posts/:id,
DELETE /posts/:id.

We tested it and everything works as expected. Does it mean our API is ready for production? Technically, we can use it as we already tested it during the development process. But we can do something to minimize the occurrence of bugs because does not rule out the possibility that our API will be grown and will have more features in the future.

What we can do? In the software development world, it's common to write code for testing purposes. We'll have advantages for writing testing code (especially in regression testing) that will save our time to do the automated testing instead of doing manual testing one by one from the very beginning. Not just that, writing testing code will make us more confident to release our code to production.

There are some kinds of testing, but we'll only cover unit testing and e2e (end-to-end) testing. Let's focus on unit testing first (we'll do e2e testing in the next part).

If we take a look at our current codes, we can find an example of unit testing, written in a file named app.controller.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController;

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

We can try to run the code. In the terminal, go to the root folder of the project and run npm run test
Run the test code

For more details about testing, read the official documentation here: https://docs.nestjs.com/fundamentals/testing

Let's get started 🔥

Make sure to install the testing package $ npm i --save-dev @nestjs/testing. By default, it's already installed during the Nest JS project initialization in part 1.

Before diving into the code, we need to do a small configuration. For your information, we'll use jest library (for more information about jest, click here). Open up package.json on the root project folder, and find jest property:

"jest": {
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "src",
  ...
  any other config goes here...
  ...
}
Enter fullscreen mode Exit fullscreen mode

Let's add two properties (testEnvironment and moduleNameMapper) inside jest object:

"jest": {
  "moduleFileExtensions": [
    "js",
    "json",
    "ts"
  ],
  "rootDir": "src",

  ...

  "testEnvironment": "node",
  "moduleNameMapper": {
    "^src/(.*)": "<rootDir>/$1"
  }
}
Enter fullscreen mode Exit fullscreen mode

If you don't update your jest config, you'll get an error during the import module.

Now, let's write our first unit testing code for users controller. Create a new file called users.controller.spec.ts:

// src/modules/users/users.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('UsersController', () => {
  let controller: UsersController;

  beforeAll(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [UsersController],
      providers: [UsersService],
    }).compile();

    controller = app.get<UsersController>(UsersController);
  });

  it('should be defined"', () => {
    expect(controller).toBeDefined();
  });

  describe('users controller', () => {
    // 💡 Test code goes here ...
  });
});
Enter fullscreen mode Exit fullscreen mode

I updated to beforeAll because each test uses the same resource.

then try to run npm run test again
Prisma error

Normally we will get this error because users.service is dependent on PrismaService. So we have to import PrismaService to our test code:

const app: TestingModule = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    UsersService,
    PrismaService, // 💡 add prisma service here
  ],
}).compile();
Enter fullscreen mode Exit fullscreen mode

You may wonder why users.service or users.controller doesn't need to import PrismaService but in tests, we need them to be imported? In the Nest JS module, we define the PrismaService globally via CoreModule so it can be used in any other modules. Testing code is not part of Nest JS modules, that's why we have to import PrismaService individually.

Then, try to run npm run test again after we imported PrismaService. And again, we still get an error but it's different.
jwt service error
We need to import JwtService as well to make this work:

const app: TestingModule = await Test.createTestingModule({
  controllers: [UsersController],
  providers: [
    UsersService,
    PrismaService, // 💡 add prisma service here
    {
      provide: JwtService, // 💡 add jwt service here
      useValue: {
        signAsync: jest.fn(), // 💡 mock signAsync method
      },
    },
  ],
}).compile();
Enter fullscreen mode Exit fullscreen mode

In unit testing, we will not execute the actual function instead we will just mock the process or result, that's why we mock the signAsync function using jest.fn() as a replacement for the actual process access_token: await this.jwtService.signAsync(payload), in the users.service.ts.

Now let's try again, it should work ✅

Success run

We're ready to write our test codes now 🔥
In the users.controller.ts, we have five functions; registerUser, loginUser, me, updateUser, and deleteUser.

We'll try to cover those five functions in unit testing. All the test scenarios will be written inside:

describe('users controller', () => {
  // 💡 registerUser method

  // 💡 loginUser method

  // 💡 me method

  // 💡 updateUser method

  // 💡 deleteUser method
});
Enter fullscreen mode Exit fullscreen mode

Let's start with registerUser test first:

// 💡 registerUser method
it('should register new user', async () => {
  const newUser = {
    email: 'test@user.com',
    name: 'Test User',
    password: 'password',
  };

  const mockRegisterResponse: User = {
    id: 1,
    email: 'test@user.com',
    name: 'Test User',
    password: 'password',
  };

  // delete password from response
  delete mockRegisterResponse.password;

  // 💡 See here -> we mock registerUser function from users.controller.ts
  // to return mockRegisterResponse
  jest
    .spyOn(controller, 'registerUser')
    .mockResolvedValue(mockRegisterResponse);

  // 💡 See here -> we call registerUser method from users.controller.ts
  // with newUser as parameter
  const result = await controller.registerUser(newUser);

  // 💡 See here -> we expect result to be mockRegisterResponse
  expect(result).toEqual(mockRegisterResponse);
});
Enter fullscreen mode Exit fullscreen mode

In the code above, we've written the success register test and if you run npm run test will be marked as a success.

We can write for the failed register test as well:

// 💡 test register error due to email already registered
it('should throw error if email already registered', async () => {
  const registeredUser = {
    email: 'registered@user.com',
    name: 'Registered User',
    password: 'password',
  };

  jest
    .spyOn(controller, 'registerUser')
    .mockRejectedValue(new ConflictException());

  const register = controller.registerUser(registeredUser);

  await expect(register).rejects.toThrow(ConflictException);
});

// 💡 test register error due to missing some fields
it('should throw error if required fields is missing', async () => {
  jest
    .spyOn(controller, 'registerUser')
    .mockRejectedValue(new BadRequestException());

  const register = controller.registerUser(null);

  await expect(register).rejects.toThrow(BadRequestException);
});
Enter fullscreen mode Exit fullscreen mode

Quick overview. We created three test cases for register:

  • test register success,
  • test register error due to email already registered, and
  • test register error due to missing some fields.

Let's continue to loginUser. First, we'll create the success login test case:

// 💡 test login success
it('should login user', async () => {
  const mockLoginResponse = {
    access_token:
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyeyey',
  };

  // 💡 See here -> we mock loginUser function from users.controller.ts
  // to return mockLoginResponse
  jest
    .spyOn(controller, 'loginUser')
    .mockResolvedValue(mockLoginResponse);

  // 💡 See here -> we call loginUser method from users.controller.ts
  // with mockLoginResponse as parameter
  const result = await controller.loginUser({
    email: 'some@user.com',
    password: 'password',
  });

  expect(result).toEqual(mockLoginResponse);
  expect(result.access_token).toBeDefined();
});
Enter fullscreen mode Exit fullscreen mode

and test case for login error with the wrong email:

 // 💡 test login error due to wrong email
it('should throw error if email is wrong', async () => {
  const wrongEmail = {
    email: 'wrong@user.com',
    password: 'password',
  };

  jest
    .spyOn(controller, 'loginUser')
    .mockRejectedValue(new NotFoundException());

  const login = controller.loginUser(wrongEmail);

  await expect(login).rejects.toThrow(NotFoundException);
});
Enter fullscreen mode Exit fullscreen mode

Quick overview. We created two test cases for login:

  • test login success
  • test login error due to wrong email

We're done with loginUser and registerUser test cases. There are still three methods remaining; me, updateUser, and deleteUser.

I won't write all the test cases in this article. But don't worry. The full version is in my GitHub repository 🔗

Check the user full test code here: https://github.com/alfism1/nestjs-api/blob/part-four/src/modules/users/users.controller.spec.ts

Now we're done with this part :)

The full code of part 5 can be accessed here:
https://github.com/alfism1/nestjs-api/tree/part-five

Moving on to part 6
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-6-c51

Top comments (0)