使用 NestJS/Elastic 对服务进行单元测试的正确方法是什么 [英] What is the proper way to unit test Service with NestJS/Elastic

查看:26
本文介绍了使用 NestJS/Elastic 对服务进行单元测试的正确方法是什么的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试对使用弹性搜索的服务进行单元测试.我想确保我使用了正确的技术.

我是这个问题的许多领域的新用户,所以我的大部分尝试都是通过阅读与此类似的其他问题并尝试在我的用例中有意义的问题.我相信我缺少 createTestingModule 中的一个字段.有时我也会看到 providers: [Service] 和其他 components: [Service].

 const 模块:TestingModule = await Test.createTestingModule({提供者:[PoolJobService],}).编译()

这是我当前的错误:

 Nest 无法解析 PoolJobService (?) 的依赖关系.请确保索引 [0] 处的参数在 _RootTestModule 上下文中可用.

这是我的代码:

PoolJobService

import { Injectable } from '@nestjs/common'从 '../ElasticSearch/ElasticSearchService' 导入 { ElasticSearchService }@Injectable()导出类 PoolJobService {构造函数(私有只读 esService:ElasticSearchService){}异步 getPoolJobs() {返回 this.esService.getElasticSearchData('pool/job')}}

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'从 './PoolJobService' 导入 { PoolJobService }描述('PoolJobService',()=> {让 poolJobService:PoolJobServicebeforeEach(async() => {const 模块:TestingModule = await Test.createTestingModule({提供者:[PoolJobService],}).编译()poolJobService = module.get(PoolJobService)})it('应该被定义', () => {期望(poolJobService).toBeDefined()})

我也可以对此使用一些见解,但由于当前问题无法正确测试

 it('应该返回所有 PoolJobs', async() => {笑话.spyOn(poolJobService, 'getPoolJobs').mockImplementation(() => Promise.resolve([]))期望(等待 poolJobService.getPoolJobs()).resolves.toEqual([])})})

解决方案

首先,您使用 providers 是正确的.Components 是 Nest 中不存在的 Angular 特定的东西.我们拥有的最接近的东西是 controllers.

您应该为单元测试做的是测试单个函数的返回值,而无需深入挖掘代码库本身.在您提供的示例中,您可能希望使用 jest.mock 模拟您的 ElasticSearchServices 并断言 PoolJobService 方法的返回.

正如您已经指出的那样,

Nest 使用 Test.createTestingModule 为我们提供了一种非常好的方法来做到这一点.您的解决方案将类似于以下内容:

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'从 './PoolJobService' 导入 { PoolJobService }从 '../ElasticSearch/ElasticSearchService' 导入 { ElasticSearchService }描述('PoolJobService',()=> {让 poolJobService:PoolJobServicelet elasticService: ElasticSearchService//这一行是可选的,但我发现它在覆盖模拟功能时很有用beforeEach(async() => {const 模块:TestingModule = await Test.createTestingModule({提供者:[池作业服务,{提供:ElasticSearchService,使用价值:{getElasticSearchData: jest.fn()}}],}).编译()poolJobService = module.get(PoolJobService)elasticService = module.get(ElasticSearchService)})it('应该被定义', () => {期望(poolJobService).toBeDefined()})it('应该给出预期的回报', async() => {elasticService.getElasticSearchData = jest.fn().mockReturnValue({data: '你的对象在这里'})const poolJobs = 等待 poolJobService.getPoolJobs()期望(poolJobs).toEqual({数据:'你的对象在这里'})})

您可以使用 jest.spy 而不是 mock 来实现相同的功能,但这取决于您希望如何实现该功能.

作为一个基本规则,无论你的构造函数中有什么,你都需要模拟它,只要你模拟它,被模拟对象的构造函数中的任何东西都可以被忽略.测试愉快!

编辑 6/27/2019

关于我们为什么模拟 ElasticSearchService:单元测试旨在测试特定的代码段,而不是与测试函数之外的代码进行交互.在本例中,我们正在测试 PoolJobService 类的函数 getPoolJobs.这意味着我们真的不需要全力以赴连接到数据库或外部服务器,因为如果服务器关闭/修改我们不想修改的数据,这可能会使我们的测试变慢/容易中断.相反,我们模拟外部依赖项 (ElasticSearchService) 以返回一个我们可以控制的值(理论上这看起来与真实数据非常相似,但是对于我提出的这个问题的上下文它是一个字符串).然后我们测试 getPoolJobs 返回 ElasticSearchServicegetElasticSearchData 函数返回的值,因为这是该函数的功能.

在这种情况下,这看起来相当微不足道,可能看起来没有用,但是当外部调用之后开始有业务逻辑时,我们就很清楚为什么要模拟.假设我们在从 getPoolJobs 方法返回之前进行了某种数据转换以使字符串大写

导出类 PoolJobService {构造函数(私有只读 elasticSearchService:ElasticSearchService){}getPoolJobs(数据:任何):字符串{const returnData = this.elasticSearchService.getElasticSearchData(data);返回 returnData.toUpperCase();}}

从这里在测试中,我们可以告诉 getElasticSearchData 返回什么,并轻松断言 getPoolJobs 是否执行了必要的逻辑(断言字符串确实是大写的),而无需担心getElasticSearchData 中的逻辑或有关进行任何网络调用的逻辑.对于一个只返回另一个函数输出的函数,它确实感觉有点像在你的测试中作弊,但实际上你不是.您正在遵循社区中大多数其他人使用的测试模式.

当您继续进行 integratione2e 测试时,您将需要外部标注并确保您的搜索查询返回您期望的内容,但这超出了单元测试的范围.

Im trying to unit test a Service that uses elastic search. I want to make sure I am using the right techniques.

I am new user to many areas of this problem, so most of my attempts have been from reading other problems similar to this and trying out the ones that make sense in my use case. I believe I am missing a field within the createTestingModule. Also sometimes I see providers: [Service] and others components: [Service].

   const module: TestingModule = await Test.createTestingModule({
      providers: [PoolJobService],
    }).compile()

This is the current error I have:

    Nest can't resolve dependencies of the PoolJobService (?). 
    Please make sure that the argument at index [0] 
    is available in the _RootTestModule context.

Here is my code:

PoolJobService

import { Injectable } from '@nestjs/common'
import { ElasticSearchService } from '../ElasticSearch/ElasticSearchService'

@Injectable()
export class PoolJobService {
  constructor(private readonly esService: ElasticSearchService) {}

  async getPoolJobs() {
    return this.esService.getElasticSearchData('pool/job')
  }
}

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'
import { PoolJobService } from './PoolJobService'

describe('PoolJobService', () => {
  let poolJobService: PoolJobService

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [PoolJobService],
    }).compile()

    poolJobService = module.get<PoolJobService>(PoolJobService)
  })

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

I could also use some insight on this, but haven't been able to properly test this because of the current issue

  it('should return all PoolJobs', async () => {
    jest
      .spyOn(poolJobService, 'getPoolJobs')
      .mockImplementation(() => Promise.resolve([]))

    expect(await poolJobService.getPoolJobs()).resolves.toEqual([])
  })
})

解决方案

First off, you're correct about using providers. Components is an Angular specific thing that does not exist in Nest. The closest thing we have are controllers.

What you should be doing for a unit test is testing what the return of a single function is without digging deeper into the code base itself. In the example you've provided you would want to mock out your ElasticSearchServices with a jest.mock and assert the return of the PoolJobService method.

Nest provides a very nice way for us to do this with Test.createTestingModule as you've already pointed out. Your solution would look similar to the following:

PoolJobService.spec.ts

import { Test, TestingModule } from '@nestjs/testing'
import { PoolJobService } from './PoolJobService'
import { ElasticSearchService } from '../ElasticSearch/ElasticSearchService'

describe('PoolJobService', () => {
  let poolJobService: PoolJobService
  let elasticService: ElasticSearchService // this line is optional, but I find it useful when overriding mocking functionality

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PoolJobService,
        {
          provide: ElasticSearchService,
          useValue: {
            getElasticSearchData: jest.fn()
          }
        }
      ],
    }).compile()

    poolJobService = module.get<PoolJobService>(PoolJobService)
    elasticService = module.get<ElasticSearchService>(ElasticSearchService)
  })

  it('should be defined', () => {
    expect(poolJobService).toBeDefined()
  })
  it('should give the expected return', async () => {
    elasticService.getElasticSearchData = jest.fn().mockReturnValue({data: 'your object here'})
    const poolJobs = await poolJobService.getPoolJobs()
    expect(poolJobs).toEqual({data: 'your object here'})
  })

You could achieve the same functionality with a jest.spy instead of a mock, but that is up to you on how you want to implement the functionality.

As a basic rule, whatever is in your constructor, you will need to mock it, and as long as you mock it, whatever is in the mocked object's constructor can be ignored. Happy testing!

EDIT 6/27/2019

About why we mock ElasticSearchService: A unit test is designed to test a specific segment of code and not make interactions with code outside of the tested function. In this case, we are testing the function getPoolJobs of the PoolJobService class. This means that we don't really need to go all out and connect to a database or external server as this could make our tests slow/prone to breaking if the server is down/modify data we don't want to modify. Instead, we mock out the external dependencies (ElasticSearchService) to return a value that we can control (in theory this will look very similar to real data, but for the context of this question I made it a string). Then we test that getPoolJobs returns the value that ElasticSearchService's getElasticSearchData function returns, as that is the functionality of this function.

This seems rather trivial in this case and may not seem useful, but when there starts to be business logic after the external call then it becomes clear why we would want to mock. Say that we have some sort of data transformation to make the string uppercase before we return from the getPoolJobs method

export class PoolJobService {

  constructor(private readonly elasticSearchService: ElasticSearchService) {}

  getPoolJobs(data: any): string {
    const returnData = this.elasticSearchService.getElasticSearchData(data);
    return returnData.toUpperCase();
  }
}

From here in the test we can tell getElasticSearchData what to return and easily assert that getPoolJobs does it's necessary logic (asserting that the string really is upperCased) without worrying about the logic inside getElasticSearchData or about making any network calls. For a function that does nothing but return another functions output, it does feel a little bit like cheating on your tests, but in reality you aren't. You're following the testing patterns used by most others in the community.

When you move on to integration and e2e tests, then you'll want to have your external callouts and make sure that your search query is returning what you expect, but that is outside the scope of unit testing.

这篇关于使用 NestJS/Elastic 对服务进行单元测试的正确方法是什么的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆