12
votes

This question can likely be generalized to stubbing repositories in a service and how to properly test and provide coverage in the context of this question.

I am in the process of learning more about testing, but am stuck with how to properly perform testing that involves the DB.

I have a User entity that defines the columns and some initial validation logic.

    import { IsAlphanumeric, IsEmail, MinLength } from 'class-validator';
    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    @Entity()
    export class User {
      @PrimaryGeneratedColumn()
      public id!: number;

      @Column()
      public name!: string;

      @IsEmail()
      @Column()
      public email!: string;

      @MinLength(8)
      @Column()
      public password!: string;
    }

And I have a UserService that injects the Repository for the entity.

    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { validateOrReject } from 'class-validator';
    import { Repository } from 'typeorm';
    import { CreateUserDTO } from './dto/create-user.dto';
    import { User } from './user.entity';

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

      public async create(dto: CreateUserDTO) {
        const user = this.userRepository.create(dto);
        await validateOrReject(user);
        await this.userRepository.save(user);
      }

      public async findAll(): Promise<User[]> {
        return await this.userRepository.find();
      }

      public async findByEmail(email: string): Promise<User | undefined> {
        return await this.userRepository.findOne({
          where: {
            email,
          },
        });
      }
    }

And here is my preliminary test so you can follow my train of thought...

    import { Test, TestingModule } from '@nestjs/testing';
    import { getRepositoryToken } from '@nestjs/typeorm';
    import { User } from './user.entity';
    import { UserService } from './user.service';

    const createMock = jest.fn((dto: any) => {
      return dto;
    });

    const saveMock = jest.fn((dto: any) => {
      return dto;
    });

    const MockRepository = jest.fn().mockImplementation(() => {
      return {
        create: createMock,
        save: saveMock,
      };
    });
    const mockRepository = new MockRepository();

    describe('UserService', () => {
      let service: UserService;

      beforeAll(async () => {
        const module: TestingModule = await Test.createTestingModule({
          providers: [
            UserService,
            {
              provide: getRepositoryToken(User),
              useValue: mockRepository,
            },
          ],
        }).compile();
        service = module.get<UserService>(UserService);
      });

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

      it('should not create invalid user', async () => {
        // ??
      });
    });

So while I can make the test run and everything, I am not sure what I am actually supposed to be testing. I can obviously test that it validates on create, and for other things like findAll, I feel like I am just mocking the database? For me to properly test this, would it need to be connected to a database so I can check that the right data is returned?

The nest documents say "we usually want to avoid any database connection", but doesn't doing that defeat the purpose since we aren't really testing the functionality? Because while I can mock that the save returns a value, I am not testing for any errors that can occur with unique columns, nullable data, incrementing values to be set, etc... right?

2
"I am not sure what I am actually supposed to be testing", exactly, because you basically have nothing to test in your service. The only thing you could test is if your service properly call the repository methods you want it to call and with the proper parameters and for that you will use a "spyOn" or "toHaveBeenCalledWith". TypeORM is already a tested library, so you don't want to test TypeORM. Testing against a database is a functional test, not a unit test, in that case, I would reorganize your test in a End-to-End fashion, calling your API endpoint and testing the database after the call.Eric Jeker

2 Answers

13
votes

Many see it as bad practice to test against a db. But for exactly the reasons you mention + saving myself the hassle of managing the mocks and stubs, I nearly always run my tests against a dedicated test-database.

In my jest start-up I clear out all tables and then have helpers which help me create entities with relations as needed, to ensure that my test remain atomic.

1
votes

What @AyKarsi suggest is better than nothing, but it's still a bad practice.

Unit testing should mock databases and third party API calls.

Integration testing should test what has been mocked with the real database, and that part only.

End-to-end testing is there to check that the whole app is well connected altogether.

For more details, you can read : https://martinfowler.com/articles/practical-test-pyramid.html