首页>>前端>>JavaScript->快速入门nest.js(10/10)

快速入门nest.js(10/10)

时间:2023-12-01 本站 点击:0

初识Jest

很好的错误消息和内置Mocking实用程序

可靠的并行运行测试

优先运行以前失败的测试

根据测试文件花费的时间重新组织测试运行

// npm run test  # for unit tests// npm run test:cov  # for test coverage// npm run test:e2e  # for e2e tests

注意: 测试程序不支持绝对路径的导入,VSCODE自动导入的需要换成相对路径

开始unit

对于nest的单元测试,通常的做法是将.spec.ts文件保存在与它们测试的应用程序源代码相同的文件夹中。控制器、提供者、服务等都应该有自己的专用测试文件并且必须是.spec.ts后缀

对于nest的端到端测试,默认这些文件位于专用的/test/目录下,自动化的端到端测试帮助我们确保系统的整体行为是正确的。

coffees.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});

运行:npm run test:watch --coffees.service

<会出现依赖错误>

我们的TestingModule仅包含1个提供程序,即CoffeesService,理论上我们要修复错误只需将所需的提供程序添加到prociders[]中。然而,这将违反最佳实践和单元测试背后的通用理念。单元测试应该在-isolation-中执行,但这并不意味着完全隔离,隔离是指测试不应该依赖于外部依赖,单元测试的理念是在这种情况下模拟一切,但这通常会导致难以维护的脆弱测试,并且不会带来任何重大价值。我们的CoffeesService依赖于与数据库相关的提供者,但我们要做的最后一件事是实例化一个真实数据库的Connection,只是为了单元测试。所以需要其他选择:无需创建复杂的Mocks或连接到真实数据库,我们真正要做的就是确保u偶有请求的提供者都可用于TestingModule,作为临时解决方案,我们使用自定义提供程序语法来提供我们CoffeesSerice所依赖的所有类:

providers: [    CoffeesService,    { provide: Connection, useValue: {} },    { provide: getRepositoryToken(Flavor), useValue: {} },    { provide: getRepositoryToken(Coffee), useValue: {} },    // { provide: getRepositoryToken(Event), useValue: {} },],

getRepositoryToken接受一个实体,返回一个InjectionToken。为这些所有的Providers一个空对象作为值,一旦我们开始测试特定的方法,我们将用Mocks替换这些空对象。

添加单元测试

在测试包含业务逻辑的服务或类似的类时,我们更喜欢按方法对相关测试进行分组,使用方法名称作为我们的describe()块。

这里测试下面这个方法:

  async findOne(id: string) {    const coffee = await this.coffeeRepository.findOne(id, {  //我们必须确保模拟这个coffeeRepository方法才能让我们的测试正常运行      relations: ['flavors'],    });    if (!coffee) {  // 我们必须通过单元测试覆盖两种不同的场景      throw new NotFoundException(`Coffee ${id} not found`);    }    return coffee;  }

定义测试用例:

  describe('findOne', () => {    describe('when coffee with ID exists', () => {      it('should return the coffee object', async () => {        const coffeeId = '1';        const expectedCoffee = {};        const coffee = await service.findOne(coffeeId);        expect(coffee).toEqual(expectedCoffee);      });    });    describe('otherwise', () => {      it('shuold throw the "NotFountException"', async () => {});    });  });

运行会发现:

这里理所当然的,因为我们之前使用空对象作为了我们的实体,显然里面没有定义任何方法,所以这个错误时有道理的。

最好的方法是创建一个通用函数,该函数仅返回一个Mock对象,其中包含存储库类提供的所有相同方法,然后对这些方法进行stub,以根据特定条件操纵它们的行为:

// 由该存储库类型的一些属性组成,并由Jest提供的模拟函数模拟值type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;const createMockRepository = <T = any>():MockRepository<T> => ({  findOne: jest.fn(),  create: jest.fn(),})

然后替换:

{ provide: getRepositoryToken(Flavor), useValue: createMockRepository() },{ provide: getRepositoryToken(Coffee), useValue: createMockRepository() },

然后第二步我们需要在我们的测试函数中使用coffeeRespository变量,所以我们需要判断其是否定义。

describe('CoffeesService', () => {  let service: CoffeesService;  let coffeeRepository: MockRepository;    // ...

  beforeEach(async () => {// ...    service = module.get<CoffeesService>(CoffeesService); // 然后存储在该变量中    coffeeRepository = module.get<MockRepository>(getRepositoryToken(Coffee));  });

模拟对应的方法:

      it('should return the coffee object', async () => {        const coffeeId = '1';        const expectedCoffee = {};        coffeeRepository.findOne.mockReturnValue(expectedCoffee);  // 这里模拟了返回值        const coffee = await service.findOne(coffeeId);        expect(coffee).toEqual(expectedCoffee);      });

最后测试成功:

接下来完成失败路径的测试逻辑:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});0

最后也成功:

完整代码:

import { NotFoundException } from '@nestjs/common';import { Test, TestingModule } from '@nestjs/testing';import { getRepositoryToken } from '@nestjs/typeorm';import exp from 'constants';import { Connection, Repository } from 'typeorm';import { CoffeesService } from './coffees.service';import { Coffee } from './entities/coffee.entity';import { Flavor } from './entities/flavor.entity';// import { Event } from 'src/events/entities/event.entity';// 由该存储库类型的一些属性组成,并由Jest提供的模拟函数模拟值type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;const createMockRepository = <T = any>():MockRepository<T> => ({  findOne: jest.fn(),  create: jest.fn(),})// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  let coffeeRepository: MockRepository;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [        CoffeesService,        { provide: Connection, useValue: {} },        { provide: getRepositoryToken(Flavor), useValue: createMockRepository() },        { provide: getRepositoryToken(Coffee), useValue: createMockRepository() },        // { provide: getRepositoryToken(Event), useValue: {} },      ],    }).compile(); // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService); // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序    coffeeRepository = module.get<MockRepository>(getRepositoryToken(Coffee));  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });  describe('findOne', () => {    describe('when coffee with ID exists', () => {      it('should return the coffee object', async () => {        const coffeeId = '1';        const expectedCoffee = {};        coffeeRepository.findOne.mockReturnValue(expectedCoffee);        const coffee = await service.findOne(coffeeId);        expect(coffee).toEqual(expectedCoffee);      });    });    import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});0  });});

开始e2e

初始文件:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});2

运行:npm run test:e2e

这个警告意味着有一些异步操作在我们的测试中没有终止,你需要关闭应用程序:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});3

最终代码:

import { Test, TestingModule } from '@nestjs/testing';import { INestApplication } from '@nestjs/common';import * as request from 'supertest'; // 用于测试HTTP应用的高级抽象包import { AppModule } from './../src/app.module';describe('AppController (e2e)', () => {  let app: INestApplication;  beforeAll(async () => {    // 我们不想为每个端到端测试重新创建应用程序    const moduleFixture: TestingModule = await Test.createTestingModule({      imports: [AppModule],    }).compile();    // 实例化一个实际的Nest运行时环境,而不是单元测试中保存对service的引用    app = moduleFixture.createNestApplication();    await app.init();  });  it('/ (GET)', () => {    return request(app.getHttpServer())      .get('/')      .expect(200)      .expect('Hello World!');  });import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});3});

创建e2e测试

在test文件夹下创建/coffee/文件夹,并在内创建coffee.e2e-spec.ts

import { Test, TestingModule } from '@nestjs/testing';import { INestApplication } from '@nestjs/common';import { CoffeesModule } from '../../src/coffees/coffees.module';describe('[Feature] Coffees - /coffees', () => {  let app: INestApplication;  beforeAll(async () => {    const moduleFixture: TestingModule = await Test.createTestingModule({      imports: [CoffeesModule],    }).compile();    app = moduleFixture.createNestApplication();    await app.init();  });import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});3});

加入待做事项提醒:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});6

但显然此时运行会出现之前见过的依赖错误,就是没有连接数据库,总的来说有三种方法解决:

mock

使用较为简单的SQLite替代

直接使用原数据库postgresql

这里用第三种方法:

打开docker-compose文件

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});7

然后在package.json下添加脚本简化操作:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});8

回到coffees.e2e-spec文件并导入TypeOrmModule.forRoot()进行初始化:

import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});9

createTestingModule会创建一个应用实例,我们需要将main.ts中的配置全部添加到该文件下:

providers: [    CoffeesService,    { provide: Connection, useValue: {} },    { provide: getRepositoryToken(Flavor), useValue: {} },    { provide: getRepositoryToken(Coffee), useValue: {} },    // { provide: getRepositoryToken(Event), useValue: {} },],0

使用需要jasmine需要安装并添加最后一行:

providers: [    CoffeesService,    { provide: Connection, useValue: {} },    { provide: getRepositoryToken(Flavor), useValue: {} },    { provide: getRepositoryToken(Coffee), useValue: {} },    // { provide: getRepositoryToken(Event), useValue: {} },],1

添加逻辑之后的代码:

import { Test, TestingModule } from '@nestjs/testing';import { HttpStatus, INestApplication, ValidationPipe } from '@nestjs/common';import { CoffeesModule } from '../../src/coffees/coffees.module';import { TypeOrmModule } from '@nestjs/typeorm';import * as request from 'supertest';import { CreateCoffeeDto } from 'src/coffees/dto/create-coffee.dto';describe('[Feature] Coffees - /coffees', () => {  const coffee = {    name: 'Shipwreack Roast',    brand: 'Buddy Brew',    flavors: ['chocolate', 'vanilla'],  };  let app: INestApplication;  beforeAll(async () => {    const moduleFixture: TestingModule = await Test.createTestingModule({      imports: [        CoffeesModule,        TypeOrmModule.forRoot({          type: 'postgres',          host: 'localhost',          port: +5433, // 注意这里使用的是不同的端口          username: 'postgres',          password: 'pass123',          database: 'postgres',          autoLoadEntities: true,          synchronize: true,        }),      ],    }).compile();    app = moduleFixture.createNestApplication();    app.useGlobalPipes(      new ValidationPipe({        whitelist: true,        transform: true,        forbidNonWhitelisted: true,        transformOptions: {          enableImplicitConversion: true,        },      }),    );    await app.init();  });  it('Create [POST /]', () => {    return request(app.getHttpServer())    .post('/coffees')    .send(coffee as CreateCoffeeDto)    .expect(HttpStatus.CREATED)    .then(({body})=>{      // 使用jasmine进行部分撇皮,当期望在执行实际而是时只关心某些键\值对时很有用。      const expectdCoffee = jasmine.objectContaining({        ...coffee,        flavors: jasmine.arrayContaining(  // 每种flavor在应用中都是一个实体          coffee.flavors.map(name=> jasmine.objectContaining({name})),        ),      });      expect(body).toEqual(expectdCoffee);    })  });  it.todo('Get ll [GET /]');  it.todo('Get one [GET /:id]');  it.todo('Update one [PATCH /:id]');  it.todo('Delete one [DELETE /:id]');import { Test, TestingModule } from '@nestjs/testing';import { CoffeesService } from './coffees.service';// describe块将所有与CoffeeService类相关的单元测试分组describe('CoffeesService', () => {  let service: CoffeesService;  // 在每次测试之前执行的钩子函数,称为设置阶段,出此之外,还有beforeAll(),afterEach(),afterAll()  beforeEach(async () => {    const module: TestingModule = await Test.createTestingModule({      providers: [CoffeesService],    }).compile();  // 利用这个模块获取CoffeesService,compile()引导模块及其依赖项,类似于main.ts中的bootstrap    service = module.get<CoffeesService>(CoffeesService);  // 然后存储在该变量中    // service = await module.resolve(CoffeesService);  // 检索请求范围和瞬态范围的提供程序  });  // it表达单独测试,该测试目前仅检查是否定义了service变量  it('should be defined', () => {    expect(service).toBeDefined();  });});3});

\

原文:https://juejin.cn/post/7097942551256121351


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/6413.html