Inject TypeORM repository into NestJS service for mock data testing

42,746

Solution 1

Let's assume we have a very simple service that finds a user entity by id:

export class UserService {
  constructor(@InjectRepository(UserEntity) private userRepository: Repository<UserEntity>) {
  }

  async findUser(userId: string): Promise<UserEntity> {
    return this.userRepository.findOne(userId);
  }
}

Then you can mock the UserRepository with the following mock factory (add more methods as needed):

// @ts-ignore
export const repositoryMockFactory: () => MockType<Repository<any>> = jest.fn(() => ({
  findOne: jest.fn(entity => entity),
  // ...
}));

Using a factory ensures that a new mock is used for every test.

describe('UserService', () => {
  let service: UserService;
  let repositoryMock: MockType<Repository<UserEntity>>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        // Provide your mock instead of the actual repository
        { provide: getRepositoryToken(UserEntity), useFactory: repositoryMockFactory },
      ],
    }).compile();
    service = module.get<UserService>(UserService);
    repositoryMock = module.get(getRepositoryToken(UserEntity));
  });

  it('should find a user', async () => {
    const user = {name: 'Alni', id: '123'};
    // Now you can control the return value of your mock's methods
    repositoryMock.findOne.mockReturnValue(user);
    expect(service.findUser(user.id)).toEqual(user);
    // And make assertions on how often and with what params your mock's methods are called
    expect(repositoryMock.findOne).toHaveBeenCalledWith(user.id);
  });
});

For type safety and comfort you can use the following typing for your (partial) mocks (far from perfect, there might be a better solution when jest itself starts using typescript in the upcoming major releases):

export type MockType<T> = {
  [P in keyof T]?: jest.Mock<{}>;
};

Solution 2

My solution uses sqlite memory database where I insert all the needed data and create schema before every test run. So each test counts with the same set of data and you do not have to mock any TypeORM methods:

import { Test, TestingModule } from "@nestjs/testing";
import { CompanyInfo } from '../../src/company-info/company-info.entity';
import { CompanyInfoService } from "../../src/company-info/company-info.service";
import { Repository, createConnection, getConnection, getRepository } from "typeorm";
import { getRepositoryToken } from "@nestjs/typeorm";

describe('CompanyInfoService', () => {
  let service: CompanyInfoService;
  let repository: Repository<CompanyInfo>;
  let testingModule: TestingModule;

  const testConnectionName = 'testConnection';

  beforeEach(async () => {
    testingModule = await Test.createTestingModule({
      providers: [
        CompanyInfoService,
        {
          provide: getRepositoryToken(CompanyInfo),
          useClass: Repository,
        },
      ],
    }).compile();

    let connection = await createConnection({
        type: "sqlite",
        database: ":memory:",
        dropSchema: true,
        entities: [CompanyInfo],
        synchronize: true,
        logging: false,
        name: testConnectionName
    });    

    repository = getRepository(CompanyInfo, testConnectionName);
    service = new CompanyInfoService(repository);

    return connection;
  });

  afterEach(async () => {
    await getConnection(testConnectionName).close()
  });  

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

  it('should return company info for findOne', async () => {
    // prepare data, insert them to be tested
    const companyInfoData: CompanyInfo = {
      id: 1,
    };

    await repository.insert(companyInfoData);

    // test data retrieval itself
    expect(await service.findOne()).toEqual(companyInfoData);
  });
});

I got inspired here: https://gist.github.com/Ciantic/be6a8b8ca27ee15e2223f642b5e01549

Solution 3

You can also use a test DB and insert data there.

describe('EmployeesService', () => {
  let employeesService: EmployeesService;
  let moduleRef: TestingModule;

  beforeEach(async () => {
    moduleRef = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forFeature([Employee]),
        TypeOrmModule.forRoot({
          type: 'postgres',
          host: 'db',
          port: 5432,
          username: 'postgres',
          password: '',
          database: 'test',
          autoLoadEntities: true,
          synchronize: true,
        }),
      ],
      providers: [EmployeesService],
    }).compile();

    employeesService = moduleRef.get<EmployeesService>(EmployeesService);
  });

  afterEach(async () => {
    // Free DB connection for next test
    await moduleRef.close();
  });

  describe('findOne', () => {
    it('returns empty array', async () => {
      expect(await employeesService.findAll()).toStrictEqual([]);
    });
  });
});

You will need to create the DB manually, e.g. psql -U postgres -c 'create database test;'. Schema sync will happen automatically.

Solution 4

I also found that this worked for me:

export const mockRepository = jest.fn(() => ({
  metadata: {
    columns: [],
    relations: [],
  },
}));

and

const module: TestingModule = await Test.createTestingModule({
      providers: [{ provide: getRepositoryToken(Entity), useClass: mockRepository }],
    }).compile();

Solution 5

Starting with the above ideas and to help with mocking any class, we came out with this MockFactory:

export type MockType<T> = {
    [P in keyof T]?: jest.Mock<unknown>;
};

export class MockFactory {
    static getMock<T>(type: new (...args: any[]) => T, includes?: string[]): MockType<T> {
        const mock: MockType<T> = {};

        Object.getOwnPropertyNames(type.prototype)
            .filter((key: string) => key !== 'constructor' && (!includes || includes.includes(key)))
            .map((key: string) => {
                mock[key] = jest.fn();
            });

        return mock;
    }
}

const module: TestingModule = await Test.createTestingModule({
    providers: [
        {
            provide: getRepositoryToken(MyCustomRepository),
            useValue: MockFactory.getMock(MyCustomRepository)
        }
    ]
}).compile();
Share:
42,746
nurikabe
Author by

nurikabe

Updated on July 14, 2022

Comments

  • nurikabe
    nurikabe almost 2 years

    There's a longish discussion about how to do this in this issue.

    I've experimented with a number of the proposed solutions but I'm not having much luck.

    Could anyone provide a concrete example of how to test a service with an injected repository and mock data?

  • nurikabe
    nurikabe about 5 years
    Great answer. I wasn't aware of useFactory in providers.
  • jackabe
    jackabe almost 5 years
    What is MockType?
  • Kim Kern
    Kim Kern almost 5 years
    @jackabe see the last paragraph. It's a type definition that's supposed to make using jest mocks more comfortable but it has a couple of limitations.
  • Daniel Flores
    Daniel Flores over 4 years
    In my case, I need to add await before service.findUser(user.id)
  • lokeshjain2008
    lokeshjain2008 over 4 years
    Like the approach of having a test DB. this can be further improved.
  • SalahAdDin
    SalahAdDin about 3 years
    I', getting an error for MongoRepository: Type 'Mock<{ create: Mock<any, any>; deleteOne: Mock<any, [entity: any]>; findOne: Mock<any, [entity: any]>; save: Mock<any, [entity: any]>; update: Mock<any, any>; }, []>' is not assignable to type '() => MockType<MongoRepository<any>>'. Type '{ create: Mock<any, any>; deleteOne: Mock<any, [entity: any]>; findOne: Mock<any, [entity: any]>; save: Mock<any, [entity: any]>; update: Mock<any, any>; }' is missing the following properties from type 'MockType<MongoRepository<any>>': manager, query, createQueryBuilder, find, and 54 more..
  • SalahAdDin
    SalahAdDin about 3 years
    I found the solution and I edited the answer.
  • JSEvgeny
    JSEvgeny about 3 years
    autoLoadEntities didn't work for me, so I used string path. Huge thanx for this easy setup example! It is also possible to create test_db with init migration.
  • Admin
    Admin over 2 years
    Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.
  • user192344
    user192344 about 2 years
    It doesnt work for me, i have error on " TypeError: Right-hand side of 'instanceof' is not an object" on the line of @InjectRepository(MyEntity)