jmcdo29 / testing-nestjs

A repository to show off to the community methods of testing NestJS including Unit Tests, Integration Tests, E2E Tests, pipes, filters, interceptors, GraphQL, Mongo, TypeORM, and more!
MIT License
2.88k stars 379 forks source link

[NEW TEST] Sequelize transactions #1985

Open c-kirkeby opened 1 year ago

c-kirkeby commented 1 year ago

Is there an existing issue for this?

Feature Test To Be Requested

The sequelize-sample is really awesome, but we're using Sequelize Transactions in our codebase and by doing so we have to mock the Sequelize transaction() method or Nest will throw dependency errors:

Nest can't resolve dependencies of the CatService (CatEntityRepository, ?). Please make sure that the argument Sequelize at index [5] is available in the RootTestModule context.

Potential solutions:
- Is RootTestModule a valid NestJS module?
- If Sequelize is a provider, is it part of the current RootTestModule?
- If Sequelize is exported from a separate @Module, is that module imported within RootTestModule?
  @Module({
    imports: [ /* the Module containing Sequelize */ ]
  })

I've looked through the NestJS documentation and Discord but I couldn't find any good examples of how to do this because if I mock the Sequelize transaction like this, I don't get the value from my service call:

        {
          provide: Sequelize,
          useValue: {
            transaction: jest.fn(() => Promise.resolve()),
          },
        },

The Nest docs state to use a helper factory class but I couldn't find a good example of this either.

1111mp commented 1 year ago

Is this solution correct? I don't think I'm writing code for automated testing, just trying to piece together code that doesn't report errors. : (

users.service.ts:

@Injectable()
export class UsersService {
  constructor(
    private readonly sequelize: Sequelize,
    @InjectModel(User)
    private readonly userModel: typeof User,
    @Inject(IORedisKey) private readonly redisClient: Redis,
  ) {}

  // ......

  async getUserModel(id: number): Promise<User> {
    return this.userModel.findOne({
      attributes: { exclude: ['pwd'] },
      where: { id },
    });
  }

  async findByAccount(account: string): Promise<User.UserInfo | null> {
    const trans = await this.sequelize.transaction();
    const user = await this.userModel.findOne({
      where: {
        account,
      },
      transaction: trans,
    });

    if (!user) return null;

    const role = await user.$get('role', { transaction: trans });
    const permissions = (
      await role.$get('permissions', { transaction: trans })
    ).map(({ id, name, desc }) => ({
      id,
      name,
      desc,
    }));

    await trans.commit();

    const { id: roleId, name: roleName, desc: roleDesc } = role;

    return {
      ...user.toJSON(),
      roleId,
      roleName,
      roleDesc,
      permissions,
    };
  }

 // ......
}

users.service.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { getModelToken } from '@nestjs/sequelize';
import { UsersService } from './users.service';
import { User as UserModel } from './models/user.model';
import { IORedisKey } from 'src/common/redis/redis.module';

import type { Redis } from 'ioredis';
import { Sequelize } from 'sequelize-typescript';

const testUser = {
  id: 10007,
  account: '176******',
  avatar: 'avatar.png',
  email: 'test@gmail.com',
  regisTime: '2023-01-28 12:16:06',
  updateTime: '2023-01-29 14:01:35',
};

const testRole = {
  id: 5,
  name: 'admin',
  desc: '管理员',
};

const testPermissions = {
  id: 1,
  name: 'userDel',
  desc: 'Delete user',
};

describe('UsersService', () => {
  let service: UsersService,
    model: typeof UserModel,
    redisClient: Redis,
    sequelize: Sequelize;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UsersService,
        {
          provide: Sequelize,
          useValue: {
            transaction: jest.fn(),
          },
        },
        {
          provide: getModelToken(UserModel),
          useValue: {
            findOne: jest.fn(),
            create: jest.fn(() => testUser),
          },
        },
        { provide: IORedisKey, useValue: {} },
      ],
    }).compile();

    service = module.get<UsersService>(UsersService);
    model = module.get<typeof UserModel>(getModelToken(UserModel));
    redisClient = module.get<Redis>(IORedisKey);
    sequelize = module.get<Sequelize>(Sequelize);
  });

  afterEach(() => {
    jest.restoreAllMocks();
  });

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

  it('should get a single user by the method named getUserModel', () => {
    const findSpy = jest.spyOn(model, 'findOne');

    expect(service.getUserModel(10007));
    expect(findSpy).toBeCalledWith({
      attributes: { exclude: ['pwd'] },
      where: { id: 10007 },
    });
  });

  test('the method named findByAccount', async () => {
    // mock transaction
    const commit = jest.fn();
    const transaction = jest.fn(async () => ({ commit }) as any);
    const transSpy = jest
      .spyOn(sequelize, 'transaction')
      .mockImplementation(transaction);

    const permissionsStub = jest.fn(() => ({
        map: jest.fn(() => [testPermissions]),
      })),
      roleStub = jest.fn((key: string) => ({
        ...testRole,
        $get: permissionsStub,
      }));
    const findSpy = jest.spyOn(model, 'findOne').mockReturnValue({
      $get: roleStub,
      toJSON: jest.fn(() => testUser),
    } as any);

    const retVal = await service.findByAccount('176********');

    expect(transSpy).toBeCalledTimes(1);
    expect(findSpy).toBeCalledWith({
      where: { account: '176********' },
      transaction: await transaction(),
    });
    expect(commit).toBeCalledTimes(1);

    const { id: roleId, name: roleName, desc: roleDesc } = testRole;

    expect(retVal).toEqual({
      ...testUser,
      roleId,
      roleName,
      roleDesc,
      permissions: [testPermissions],
    });
  });
});
image

These codes look kind of stupid ...... Can anyone suggest the correct solution (or any idea).