63

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?

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
nurikabe
  • 3,802
  • 2
  • 31
  • 39

8 Answers8

131

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<{}>;
};
Kim Kern
  • 54,283
  • 17
  • 197
  • 195
23

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

michal.jakubeczy
  • 8,221
  • 1
  • 59
  • 63
  • 2
    Like the approach of having a test DB. this can be further improved. – lokeshjain2008 Dec 10 '19 at 06:46
  • If you want faster tests, you can create the SQLite DB once in `beforeAll`. Duplicate the sqlite database file, and run tests against the copy. Every time you want to reset the database state, delete the copy and copy the original again. It's considerably faster than re-creating the db every time, certainly if you have seed data. – David Barker Aug 02 '23 at 16:13
6

Similar to best practices in other frameworks you can use a test DB instead of a mock.

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

  beforeEach(async () => {
    moduleRef = await Test.createTestingModule({
      imports: [
        TypeOrmModule.forRoot({
          type: 'postgres',
          url: 'postgres://postgres:@db:5432/test', // read this from env
          autoLoadEntities: true,
          synchronize: true,
          dropSchema: true,
        }),
      ],
      providers: [EmployeesService],
    }).compile();

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

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

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

Real-life example in resolver specs in: https://github.com/thisismydesign/nestjs-starter

Last tested with typeorm@0.3.7 and @nestjs/typeorm@9.0.0.

thisismydesign
  • 21,553
  • 9
  • 123
  • 126
  • `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. – JSEvgeny Mar 21 '21 at 16:47
5

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();
Vincil Bishop
  • 1,594
  • 17
  • 21
1

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();
SPAS
  • 11
  • 1
  • 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](/help/how-to-answer). – Community Dec 17 '21 at 18:09
0

First of all I'm new to Ts/Js/Node. Here is my example code : it lets you use NEST's injection system with a custom Connection during tests. In this manner service/controller objects are not created by hand but wired by the TestingModule:

import { Test } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import {
  Repository,
  createConnection,
  getConnection,
  getRepository,
  Connection,
} from 'typeorm';
import { Order } from './order';
import { OrdersService } from './orders.service';

describe('Test Orders', () => {
  let repository: Repository<Order>;
  let service: OrdersService;
  let connection: Connection;
  beforeEach(async () => {
    connection = await createConnection({
      type: 'sqlite',
      database: './test.db',
      dropSchema: true,
      entities: [Order],
      synchronize: true,
      logging: true,
    });
    repository = getRepository(Order);
    const testingModule = await Test.createTestingModule({
      providers: [
        OrdersService,
        {
          provide: getRepositoryToken(Order, connection),
          useFactory: () => {
            return repository;
          },
        },
      ],
    }).compile();
    console.log('Getting Service from NEST');
    service = testingModule.get<OrdersService>(OrdersService);
    return connection;
  });

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

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

  it('CRUD Order Test', async () => {
    const order = new Order();
    order.currency = 'EURO';
    order.unitPrice = 12.0;
    order.issueDate = new Date();
    const inserted = await service.create(order);
    console.log('Inserted order ', inserted.id); // id is the @PrimaryGeneratedColumn() key
    let allOrders = await service.findAll();
    expect(allOrders.length).toBe(1);
    await service.delete(inserted.id);
    allOrders = await service.findAll();
    expect(allOrders.length).toBe(0);
  });
});
Mike
  • 335
  • 1
  • 8
0

Something similar to the suggested MockTypes defined in the previous answer is the TypedMockType

type ArgsType<T> = T extends (...args: infer A) => unknown ? A : never;

export type TypedMockType<T> = {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  [P in keyof T]: T[P] extends (...args: any) => unknown
    ? jest.Mock<ReturnType<T[P]>, ArgsType<T[P]>>
    : never;
};

This is a utility type that can be used the same as MockType, but the difference is that your payloads of the original method signature will be the same.

MathGainz
  • 320
  • 3
  • 11
0

Finally, find a working example. In my case, I use a createQueryBulder in the code

this.repository
  .createQueryBuilder(tableName)
  .select("table_id AS id, name")
  .where(`${tableName}.productId='${id}'`)
  .orWhere(`${tableName}.productNumber='${id}'`)
  .getRawMany();

To test this I need to mock all the callbacks from createQueryBuilder:

describe("GET", () => {
it("should return a record", async () => {
  const getRawMany = jest.fn();
  const orWhere = jest.fn(() => ({ getRawMany }));
  const where = jest.fn(() => ({ orWhere }));
  const select = jest.fn(() => ({ where }));
  spyRepository.createQueryBuilder = jest.fn(() => ({ select }));

  await service.findOneById(productId);
  expect(spyRepository.createQueryBuilder).toHaveBeenCalledWith(tableName);
  expect(where).toHaveBeenCalledWith(`${tableName}.productId='${Number(id)}'`);
  expect(orWhere).toHaveBeenCalledWith(`${tableName}.productNumber='${String(id)}'`);
});

});

So, the answer is, you need to mock all the methods you're using.

chavy
  • 841
  • 10
  • 20