快速入门nest.js(10/10)--测试

简介: 初识Jest很好的错误消息和内置Mocking实用程序可靠的并行运行测试优先运行以前失败的测试根据测试文件花费的时间重新组织测试运行

初识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,以根据特定条件操纵它们的行为:

swift

// 由该存储库类型的一些属性组成,并由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);
      });


最后测试成功:

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


    
describe('otherwise', () => {
      it('shuold throw the "NotFountException"', async () => {
        const coffeeId = '1';
        coffeeRepository.findOne.mockReturnValue(undefined);
        try{
          await service.findOne(coffeeId);
        }catch(err){
          expect(err).toBeInstanceOf(NotFoundException);
          expect(err.message).toEqual(`Coffee ${coffeeId} not found`);
        }
      });
    });

最后也成功:

完整代码:

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);
      });
    });
    describe('otherwise', () => {
      it('shuold throw the "NotFountException"', async () => {
        const coffeeId = '1';
        coffeeRepository.findOne.mockReturnValue(undefined);
        try{
          await service.findOne(coffeeId);
        }catch(err){
          expect(err).toBeInstanceOf(NotFoundException);
          expect(err.message).toEqual(`Coffee ${coffeeId} not found`);
        }
      });
    });
  });
});


开始e2e


初始文件:

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;
  beforeEach(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('/')
      // .set('Authorization', process.env.API_KEY)
      .expect(200)
      .expect('Hello World!');
  });
});


运行:npm run test:e2e

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


    
  afterAll(async () => {
    await app.close();
  });


最终代码:

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!');
  });
  afterAll(async () => {
    await app.close();
  });
});


创建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();
  });
  afterAll(async () => {
    await app.close();
  });
});

    


加入待做事项提醒:

  it.todo('Create [POST /]');
  it.todo('Get ll [GET /]');
  it.todo('Get one [GET /:id]');
  it.todo('Update one [PATCH /:id]');
  it.todo('Delete one [DELETE /:id]');


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

  • mock
  • 使用较为简单的SQLite替代
  • 直接使用原数据库postgresql

这里用第三种方法:

打开docker-compose文件

version: '3'
services:
  db:
    image: postgres
    restart: always
    ports:
      ["5432:5432"]
    environment:
      POSTGRES_PASSWORD: pass123
  test-db:
  image: postgres
  restart: always
  ports:
    ["5433:5432"]
  environment:
    POSTGRES_PASSWORD: pass123


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

  "scripts": {
// ...
    "pretest:e2e":"docker-compose up -d test-db",   // 测试前创建
    "test:e2e": "jest --config ./test/jest-e2e.json",
    "posttest:e2e":"docker-compose stop test-db && docker-compose rm -f test-db"  //测试后删除
  },
// npm允许我们添加生命周期钩子脚本,在对应的脚本名上加上'pre'和'post'前缀,就会在该脚本执行前后自动执行对应命令。 、


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

describe('[Feature] Coffees - /coffees', () => {
  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();
// ...
});


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

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [
// ...
      ],
    }).compile();
    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({  // 这里
      whitelist: true,
      transform: true,
      forbidNonWhitelisted: true,
      transformOptions: {
        enableImplicitConversion: true,
      },
    }));
    await app.init();
  });


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

// jest-e2e.json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\.(t|j)s$": "ts-jest"
  },
  "testRunner":"jasmine2"
}


添加逻辑之后的代码:

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]');
  afterAll(async () => {
    await app.close();
  });
});



目录
相关文章
|
2天前
|
数据采集 人工智能 自然语言处理
Midscene.js:AI 驱动的 UI 自动化测试框架,支持自然语言交互,生成可视化报告
Midscene.js 是一款基于 AI 技术的 UI 自动化测试框架,通过自然语言交互简化测试流程,支持动作执行、数据查询和页面断言,提供可视化报告,适用于多种应用场景。
58 1
Midscene.js:AI 驱动的 UI 自动化测试框架,支持自然语言交互,生成可视化报告
|
3月前
|
Web App开发 JavaScript 前端开发
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
添加浮动按钮点击滚动到网页底部的纯JavaScript演示代码 IE9、11,Maxthon 1.6.7,Firefox30、31,360极速浏览器7.5.3.308下测试正常
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
138 1
|
2月前
|
人工智能 监控 JavaScript
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
模拟依赖关系和 AI 是Vue.js测试的下一个前沿领域
35 1
|
2月前
|
JavaScript 前端开发
JavaScript - 测试 Prototype
JavaScript - 测试 Prototype
17 0
|
2月前
|
JavaScript 前端开发
JavaScript - 测试 jQuery
JavaScript - 测试 jQuery
23 0
|
4月前
|
Web App开发 应用服务中间件 定位技术
three.js:三维模型加载量测试
three.js:三维模型加载量测试
210 4
|
4月前
|
JavaScript 前端开发 测试技术
Vue.js开发者必看!Vue Test Utils携手端到端测试,打造无懈可击的应用体验,引领前端测试新风尚!
【8月更文挑战第30天】随着Vue.js的普及,构建可靠的Vue应用至关重要。测试不仅能确保应用质量,还能提升开发效率。Vue Test Utils作为官方测试库,方便进行单元测试,而结合端到端(E2E)测试,则能构建全面的测试体系,保障应用稳定性。本文将带你深入了解如何使用Vue Test Utils进行单元测试,通过具体示例展示如何测试组件行为;并通过Cypress进行E2E测试,确保整个应用流程的正确性。无论是单元测试还是E2E测试,都能显著提高Vue应用的质量,让你更加自信地交付高质量的应用。
84 0
|
4月前
|
JavaScript 前端开发 应用服务中间件
【qkl】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
【区块链】JavaScript连接web3钱包,实现测试网络中的 Sepolia ETH余额查询、转账功能
139 0
|
6月前
|
JavaScript 前端开发 安全
【JavaScript 】DOM操作快速入门
【JavaScript 】DOM操作快速入门
98 2