【问题标题】:What is the proper way to unit test Service with NestJS/Elastic使用 NestJS/Elastic 对服务进行单元测试的正确方法是什么
【发布时间】:2019-11-08 22:16:03
【问题描述】:

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

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

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

这是我目前遇到的错误:

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

这是我的代码:

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()
  })

我也可以对此有所了解,但由于当前问题而无法正确测试

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

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

【问题讨论】:

    标签: elasticsearch graphql nestjs typegraphql


    【解决方案1】:

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

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

    正如您已经指出的那样,Nest 为我们提供了一种非常好的方法来使用Test.createTestingModule 执行此操作。您的解决方案类似于以下内容:

    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'})
      })
    
    

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

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

    编辑 2019 年 6 月 27 日

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

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

    export class PoolJobService {
    
      constructor(private readonly elasticSearchService: ElasticSearchService) {}
    
      getPoolJobs(data: any): string {
        const returnData = this.elasticSearchService.getElasticSearchData(data);
        return returnData.toUpperCase();
      }
    }
    

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

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

    【讨论】:

    • 太棒了!这似乎是正确的解决方案。不过我需要澄清一下。我的 ElasticSearchService 在其构造函数中也有一个注入服务,但我们并不关心它,因为这个解决方案完全模拟了 ElasticSearchService。它是否正确?在阅读此解决方案之前,我还想出了一个像这样的解决方案``` const module: TestingModule = await Test.createTestingModule({ providers: [PoolJobService, ElasticSearchService, APIService], }).compile() ```。我的理论是 createTestingModule 正在为我们做所有的模拟。这是错的吗?
    • 正确,我们不关心 ElasticSearchService 的依赖关系,因为服务本身是模拟的。如果您改用建议的providers: [PoolJobService, ElasticSearchService, APIService],则需要提供ElasticSearchServiceAPIService 的所有依赖项,因为Nest 否则只会实例化默认类(即运行服务器时正在运行的类),并且需要访问正确实例化这些类的所有依赖项。 createTestingModule 不会为您模拟任何内容,但允许您使用模拟来代替完整的课程。
    • 我明白了!这就说得通了。我还有另一个问题超出了这个问题的范围,但关于测试'should give the expected return'。你和我的都在尝试完成同样的测试,但我不禁觉得这不是一个有用的测试。在我看来,我们正在模拟一个函数,然后期望该模拟函数返回我们设置的值。同样,这是一个单独的问题,我缺乏测试知识,但您能解释一下为什么这是一个有用/无用的测试吗?
    • 当然,我会编辑我的答案,以便更深入地解释我为什么要模拟我展示的方式。
    • 非常感谢您提供的所有详细信息。你已经超级有帮助了!! @Jay McDoniel
    猜你喜欢
    • 2017-02-23
    • 2014-03-04
    • 2022-01-06
    • 2017-07-25
    • 1970-01-01
    • 2011-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多