Unit Test Custom Repository in NestJS

 2 total views,  2 views today

This is part 2 of Use TypeOrm Custom Repository in NestJS and this post is about unit testing custom repository.

In part one, I created a NestJS demo with controllers, services and custom repositories. The demo also added unit testing on controllers and services in which I mocked the methods of custom repositories with jest.fn(). The missing piece is unit testing on custom repository and I will fill the hole in this post.

Jest

Jest is an open source JavaScript testing framework that can use with Angular and NestJS. I used Jest to test controllers and services in part 1 and it can also be used to test custom repository.

The functions of the custom repository query database to obtain result set through TypeORM. However, there is no database running in custom repository testing and calling any repository method can lead to error. The solution is to mock the TypeORM functions such that they don’t call the database but the fake implementations to obtain test results. This is where Jest comes in because jest.fn() and jest.Spy().mockImplementation() are designed to do it.

Step 1: Create a spec file for user repository

A spec file is a Javascript/TypeScript file that encapsulates testing codes to validate the functionality of a class. It is important to import the necessary modules and providers to TestingModule before writing the first test case.

// user-repository.spec.ts

describe('UserRepository', () => {
  let service: UserRepository
  let now: Date

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserRepository],
    }).compile()

    service = module.get<UserRepository>(UserRepository)
    now = new Date(Date.now())
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })
})

Step 2: Write test cases to validate basic CRUD functions

Let remind the readers the implementation of UserRepository.

// user.repository.ts

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async getUsers(): Promise<User[]> {
    return this.find({
      order: {
        name: 'ASC',
        lastname: 'ASC',
      },
    })
  }

  async getUser(id: string): Promise<User | undefined> {
    return this.findOne(id)
  }

  async createUser(newUser: NewUser): Promise<User> {
    return this.save(this.create(newUser))
  }

  async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
  }
}

I consider getUsers, getUser and createUser basic CRUD functions because they are straightforward; calling TypeOrm functions directly to obtain results. getUserWithReports is complicated because it chains builder functions of TypeORM to compose a SQL query to obtain results from the database.

It also means less effort to mock straightforward TypeORM functions than builder functions.

Let’s write test case for getUsers function

describe('getUsers', () => {
    it('should find and return all users', async () => {
      const users: User[] = [
        { id: v4(), name: 'John', lastname: 'Doe', age: 10, createdAt: now, updatedAt: now, version: 1, reports: [] },
        { id: v4(), name: 'Jane', lastname: 'Doe', age: 22, createdAt: now, updatedAt: now, version: 1, reports: [] },
      ]
      jest.spyOn(service, 'find').mockImplementation(() => Promise.resolve(users))

      const result = await service.getUsers()
      expect(service.find).toBeCalled()
      expect(service.find).toBeCalledWith({
        order: {
          name: 'ASC',
          lastname: 'ASC',
        },
      })
      expect(result).toEqual(users)
    })
  })

Explanation:

  1. jest.spyOn(service, ‘find’).mockImplement() spies this.find() and provides a mock function that returns a promise that returns User array
  2. const result = await service.getUsers() returns the results of getUsers()
  3. expect(service.find).toBeCalled() ensures that this.find() is called within getUsers()
  4. expect(service.find).toBeCalledWith({ orders: { name: ‘ASC’, lastname: ‘ASC’ }}) ensures that this.find accepts expected argument
  5. expect(result).toEqual(users) ensures that the result is the same as the User array

I am not going to cover getUser() because it is similarly to testing getUsers()

Testing createUser() is similar to testing getUsers() but we have to mock two implementations: this.create and this.save

describe('createUser', () => {
    it('should create a new user', async () => {
      const id = v4()
      const newUser = {
        name: 'New',
        lastname: 'User',
        age: 21,
      }

      const user: User = {
        id,
        name: 'John',
        lastname: 'Doe',
        age: 10,
        createdAt: now,
        updatedAt: now,
        version: 1,
        reports: [],
      }
      jest.spyOn(service, 'create').mockImplementation(() => user)
      jest.spyOn(service, 'save').mockImplementation(() => Promise.resolve(user))

      const result = await service.createUser(newUser)
      expect(service.create).toBeCalled()
      expect(service.save).toBeCalled()
      expect(service.create).toBeCalledWith(newUser)
      expect(service.save).toBeCalledWith(service.create(newUser))
      expect(result).toEqual(user)
    })
})

Explanation:

  1. jest.spyOn(service, ‘create’).mockImplement() spies this.create() and provides a mock function that returns a new user
  2. jest.spyOn(service, ‘save’).mockImplement() spies this.save() and provides a mock function that returns a promise that returns the new user
  3. const result = await service.createUser(newUser) returns the result of createUser()
  4. expect(service.create).toBeCalled() ensures that this.create() is called within createUser()
  5. expect(service.save).toBeCalled() ensures that this.save() is called within createUser()
  6. expect(service.create).toBeCalledWith(newUser) ensures that this.create accepts expected argument
  7. expect(service.save).toBeCalledWith(service.create(newUser)) ensures that this.create is passed to this.save()
  8. expect(result).toEqual(user) ensures that the result is the same as the new user

Step 3: Write test case to validate getUserWithReports

// user.repository.ts

async getUserWithReports(id: string): Promise<User | undefined> {
    return this.createQueryBuilder('user')
      .leftJoinAndSelect('user.reports', 'reports')
      .where('user.id = :id', { id })
      .getOne()
}

Testing getUserWithReports is tricky because the builder functions are chained together and finally getOne() returns an entity to service. Unlike this.find, this.createQueryBuilder does not return entity but returning a class/object with a leftJoinAndSelect function. Similarly, leftJoinAndSelect returns a class/object with a where function and where returns a class/object with getOne function. Once you understand what they do, you can easily mock them with jest.fn().

describe('getUserWithReports', () => {
    it('should return a user with reports', async () => {
      // deleted codes that initialize other objects

      const user: User = {
        ...partialUser,
        reports: [report],
      }

      const getOne = jest.fn(() => user)
      const where = jest.fn(() => ({
        getOne,
      }))
      const leftJoinAndSelect = jest.fn(() => ({
        where,
      }))
      const createQueryBuilder = () =>
        ({
          leftJoinAndSelect,
        } as unknown as SelectQueryBuilder<User>)
      jest.spyOn(service, 'createQueryBuilder').mockImplementation(createQueryBuilder)

      const result = await service.getUserWithReports(id)
      expect(service.createQueryBuilder).toBeCalled()
      expect(service.createQueryBuilder).toBeCalledWith('user')
      expect(leftJoinAndSelect).toBeCalled()
      expect(leftJoinAndSelect).toBeCalledWith('user.reports', 'reports')
      expect(where).toBeCalled()
      expect(where).toBeCalledWith('user.id = :id', { id })
      expect(getOne).toBeCalled()
      expect(result).toEqual(user)
    })
  })

Explanation:

  1. const getOne = jest.fn(() => user) is a mock function that returns a user with reports
  2. const where = jest.fn(() => ({ getOne })) is a mock function that returns the getOne function
  3. const leftJoinAndSelect = jest.fn(() => ({ where })) is a mock function that returns the where function
  4. jest.spyOn(service, ‘createQueryBuilder’).mockImplement(createQueryBuilder) spies this.createQueryBuilder() and provides a mock function that returns the leftJoinAndSelect function
  5. const result = await service.getUserWithReports(id) returns the result of getUserWithReports()
  6. expect(service.createQueryBuilder).toBeCalled() ensures that this.createQueryBuilder() is called
  7. expect(service.createQueryBuilder).toBeCalledWith(‘report’) ensures that this.createQueryBuilder(‘report’) is called
  8. expect(leftJoinAndSelect).toBeCalled() ensures that .leftJoinAndSelect() is called
  9. expect(leftJoinAndSelect).toBeCalledWith(‘user.reports’, ‘reports’) ensures that .leftJoinAndSelect(‘user.reports’, ‘reports’) is called
  10. expect(where).toBeCalled() ensures that .where() is called
  11. expect(where).toBeCalledWith(‘user.id = :id’, { id }) ensures that .where(‘user.id = :id’, { id }) is called
  12. expect(getOne).toBeCalled() ensures that .getOne() is called
  13. expect(result).toEqual(user) ensures that result is the same as user

Final thoughts

Testing is tedious because it involves experience and long hours of practices for developers to mock APIs and functions to allow test cases to perform. Test cases should validate that correct results are received, expected functions are invoked and expected arguments are passed to the functions. Otherwise, developers do not have high confidence that the quality of the software is good before it is shipped to quality assurers for quality assurance.

I hope this article helps developers who use NestJS to build their backend applications and need help to unit test resources (controllers, services and custom repositories).

Resources:

  1. Repo: https://github.com/railsstudent/nest-poc-standard-version
  2. Clock-In/Out System Part 9: Back-End Testing — Unit Testing of Services

Dependency injection by abstract class in NestJS

 2 total views,  2 views today

When developers start using NestJS, they create new module with controllers and services to design backend APIs. Developers register controllers and services in the module and inject service into controller to perform CRUD operations.

Provider Registration

The following is a code snippet of a user module; user controller is registered in controller array while user service is registered in provider array .

@Module({
   controller: [UserController],
   provider: [UserService]
})
export class UserModule {}

[UserService] is concrete class registration and is the short form of

provider: [
  {
     provide: UserService
     useClass: UserService
  }
]

When UserService parameter is seen in the constructor of UserController, an instance of UserService injected.

Abstract Class Registration

At work, the business requirements of our application are to build different type of reports. Each type of report is encapsulated in a module and the controller defines an endpoint that exports report in PDF format and send the attachment to email users.

The naive implementation injects a service and executes functions in controller function to fulfill the purpose. The export function writes contents into a buffer stream and sendEmail attaches it to mail body and sends to some email addresses.

// User Report Module 

@Module({
   controller: [UserReportController],
   provider: [UserReportService]
})
export class UserReportModule {}
// User Report Controller

@Controller('user-report')
export class UserReportController {
   constructor(private service: UserReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       const buffer = await this.service.export()
       await this.service.sendEmail(buffer, 'user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send user report to emails:', emails)
  }
}

When there is one type of report, the codes pass our code review process. When the application enhances to support three types of reports; shareReport, export and sendEmail are copied to three modules and DRY principle is violated.

  • It is time to refactor the codes
  • It is time to refactor the codes
  • It is time to refactor the codes

Step 1: Create an abstract report service class in core directory

// Abstract report service

export abstract class AbstractReportService {
  abstract export(): Promise<Buffer>

  sendEmail(buffer: Buffer, ...emails: string[]): void {
    console.log('buffer: ', buffer.toString('utf-8'))
    console.log('send report to emails:', emails)
  }

  async shareReport(...emails: string[]): Promise<void> {
    const buffer = await this.export()
    await this.sendEmail(buffer, ...emails)
  }
}

AbstractReportService class defines shareReport function that exports PDF file and sends the buffer stream to email addresses. sendEmail is reused by different types of reports but export must be implemented by subclasses.

Step 2: Dependency injection by abstract class

Rewrite UserReportModule to provide registration of AbstractReportService

// User Report Module

@Module({
   controller: [UserReportController],
   provider: [
     { 
        provide: AbstractReportService,
        useClass: UserReportService
     }
   ]
})
export class UserReportModule {}
// User Report Controller 

@Controller('user-report')
export class UserReportController {
   constructor(private service: AbstractReportService) {}

   @POST()
   async shareReport(): Promise<void> {
       await this.service.shareReport('user1@gmail.com', 'user2@gmail.com')   
   } 
}
// User Report Service

@Injectable()
export class UserReportService {
   async export(): Promise<Buffer> {
    return Buffer.from('Export in user report service')
  }
}

Step 3: Add new report type after refactoring

When new report type is introduced to the application, the new module registers AbstractReportService and implements the abstract function. Suppose I create PaymentReport module to export a payment report.

// Payment Report Module

@Module({
  controllers: [PaymentReportController],
  providers: [
    {
      provide: AbstractReportService,
      useClass: PaymentReportService,
    },
  ],
})
export class PaymentReportModule {}
// Payment Report Controller

@Controller('payment-report')
export class PaymentReportController {
  constructor(private service: AbstractReportService) {}

  @Post()
  async shareReport(): Promise<void> {
    await this.service.shareReport('john.doe@gmail.com', 'jane.doe@gmail.com')
  }
}
// Payment Report Service

@Injectable()
export class PaymentReportService extends AbstractReportService {
  async export(): Promise<Buffer> {
    return Buffer.from('Export in payment report service')
  }
}

Step 4: CURL POST endpoints

curl -X POST "http://localhost:3000/user-report"
// buffer:  Export in user report service
// send report to emails: [ 'black.doe@gmail.com', 'white.doe@gmail.com' ]

curl -X POST "http://localhost:3000/payment-report"
// buffer:  Export in payment report service
// send report to emails: [ 'john.doe@gmail.com', 'jane.doe@gmail.com' ]

Final thoughts

Dependency injection by abstract class returns an instance of concrete service registered in @Module decorator. This solution has many benefits:

  1. DRY principle because reusable functions are defined in abstract class and subclasses implement module-specific behavior
  2. Shorter controller class since shareReport function is reduced to one line
  3. Write less testing code because shareReport and sendEmail are tested once each. Before refactoring, sendEmail is tested three times.

This is the end of the blog post! I hope you enjoy reading it and NestJS development.

Resources:

  1. Repo: https://github.com/railsstudent/nest-interface-injection
  2. https://dev.to/ef/nestjs-dependency-injection-with-abstract-classes-4g65