Angular测试性能优化-慎用TestBed.get方法

前言

随着项目业务越来越大,单元测试的case也越来越多。疏于管理和维护的单元测试,渐渐地变成了开发效率的绊脚石。每当提交代码的时候,总要跑上一段时间的单元测试,在结束时,突然告诉你要重新来过,或许因为测试挂掉,或许因为远程分支有了新的提交。

天下苦单元测试久矣!

项目测试现状

项目中的单元测试,在每次执行时,都是按照模块执行,只跑被修改模块的测试,其他模块的测试不会被执行,对跑过的单元测试文件,我们也增加了缓存机制,并且每次执行都是4个文件一起跑,在基础配上,我们对单元测试的执行效率也是有一定优化的。

除此之外,并非所有的测试文件执行效率都低下,而只有部分文件执行时间过长。有些二十多个测试case的文件甚至比四十多个测试case的文件执行时间还要长。

是时候要看看这些执行效率慢的原因了,不能让我的电脑CPU做无用功,毕竟好钢不能用在刀把上。

Jest的大致流程

首先了解一下Jest执行的一个大概流程:

  1. 读取配置文件,创建测试环境
  2. 根据配置文件和命令参数收集需要运行的测试文件和测试用例,打包编译
  3. 对于每个测试文件,Jest会为每一个describe 提供一个执行环境,并执行测试套件中的 it 用例
  4. 在执行测试用例的过程中,捕获断言并记录结果
  5. 测试完成后,将记录的结果生成报告

image.png

图片引用来自《Jest实践指南》

项目存在的问题

基于项目现有的痛点,结合执行流程上看,我关注的优化重点就集中在了执行测试用例的过程中。

通过对执行效率慢的文件进行分析,发现存在以下两个问题:

  • 随着业务的变更,在维护单元测试时,未能及时将不用的import或者注入在组件中的service移除
  • 单元测试中经常存在引入了并非当前组件或者服务需要的依赖

首先,移除了不用的import或者service,保证单元测试文件的有效性。不过对于提升效率并没有显著的提高。

接着优化第二个问题,也是最让我感到困惑的地方。举个例子,在一个页面组件中,我注入了某个service,并调用了service的a方法。a方法是一个负责http请求的方法,它会调用http service中的b方法,而b方法则是向后端发送请求,进行数据交互逻辑的地方。但是在页面组件的单元测试中,我仍然需要提供httpClient模块,似乎不是那么的合理,毕竟我没有直接调用http service。

通过对比,我发现了一个注入依赖的方式,也是这些效率低下的测试文件都会用的方式,也是我之前比较喜欢用的方式---TestBed.get 方法注入service依赖。

TestBed.get方法

TestBed.get 方法是Angular测试工具中的一部分,用于获取通过依赖注入提供的服务实例。在测试中,我们通常模拟服务的行为,以便进行更精确的断言和验证, TestBed.get 方法允许我们直接获取一个服务的实例,而不需要在每个测试中都手动创建模拟。

利与弊

TestBed.get 方法减少了我们在测试时,引入依赖的麻烦。不需要手动去初始化依赖的实例,并且不需要去mock依赖中的方法,造成我们测试文件行数的变多。

但是事务是双面性的。 TestBed.get 方法引入不必要的依赖,也就是我一直困惑的,为什么组件没有直接引用http service,但是我在测试中还需要引入相关模块或者服务。

Angular在构建测试时,会对组件或服务进行依赖树的构建。我们使用 TestBed.get 获取完整的服务实例的同时,也需要在测试中引入该服务所依赖的相关内容。这就导致我们编写一个组件测试,需要引入组件中并未使用到的,但引入的服务有所依赖的相关内容,增加了组件引入内容的庞大。

其次,我们一般都在BeforeEach 周期函数中使用TestBed.get 方法,也就意味着,每执行一个单元测试case,我们都会重新实例化一个service依赖。假设这个service功能居多,体量巨大,那我们每运行一个单元测试case的时间就会变长。小河流汇聚成大汪洋,累积下来,我们整个文件的运行时间就会变长,效率也就低下了。

第三,因为项目中使用了TestBed.get方法注入了完整的服务实例,在某些编程不规范的情况下,可能会将一个没有任何依赖的通用方法放在服务中,而非公共方法中,在跑单元测试时,我们也不容易发现这种不合理的存放。当然这种情况也不能怪在TestBed.get方法头上。

性能优化方案

针对 TestBed.get 存在的问题,可以进行了如下优化:

在使用 TestBed.get 方法,需要在 provide 中注入这个依赖,并且通过 useValue 对用到的方法进行mock。由于我们在 provide 中注入了这个依赖, TestBed.get 方法在注入时,就不再使用完整的依赖实例,而是基于 provide 提供的方法,生成实例。这里也就需要我们能够保证,组件中用到的方法,都需要在 useValue 中提供,否则测试运行就会报错

import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';


describe('MyComponent', () => {
  let dataService: DataService;


  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{
        provide: DataService,
        useValue: {
          fetchData: jest.fn()
        }
      }]
    });


    dataService = TestBed.get(DataService);
  });


  it('should fetch data', () => {
    spyOn(dataService, 'fetchData').and.returnValue(['data']);
    // 现在可以进行关于数据获取的测试
  });
});

在使用 provide 方案后,我进行了更一步的升级。既然我们提供了服务的实现,那么是否可以直接弃用 TestBed.get 方法,手动提供一个新的服务?答案是可以的。所以我最终的实现如下:

import { TestBed } from '@angular/core/testing';
import { DataService } from './data.service';


describe('MyComponent', () => {
  const fetchDataMock = jest.fn();


  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [{
        provide: DataService,
        useValue: {
          fetchData: fetchDataMock
        }
      }]
    });
  });


  it('should fetch data', () => {
    fetchDataMock.and.returnValue(['data']);
    // 现在可以进行关于数据获取的测试
  });
});

优化结果

在清空jest缓存之后,我重新各执行了一遍优化前后,当前模块的单元测试。结果如下: image.png image.png

总结

当然除了慎用 TestBed.get 方法之外,还有一些小的优化点,虽然提升效率不是那么明显,但是也多少有点作用。可以当做一个checklist,我们对比参照:

  • 减少不必要模块的导入
  • 使用 TestBed.get 时,尽量采用 provide 进行服务mock,除非你的依赖体量不大,并且依赖的依赖不多时,可以考虑直接使用 TestBed.get 方法引入,而不mock
  • 可以全局实例化的内容,通过 beforeAll 进行声明,不要放在 beforeEach
  • 针对组件依赖比较简单,不需要构建依赖树,并且不需要校验模版的情况下,使用NO_ERRORS_SCHEMA 替代CUSTOM_ELEMENTS_SCHEMA
  • 对大测试文件进行拆分,增加worker,同时跑多个测试文件

快捷的方法固然让人快乐,但是也请不要贪杯!

全部评论

相关推荐

渐好:软光栅真的写明白了吗,既然是软渲那技术栈不应该使用OpenGL,光追和bvh既不算什么高级渲染技术更不应该属于软渲的内容,git那个项目没啥用,建议把前两个项目重新组织一下语言,比如软渲染那个项目 冯着色和msaa、贴图这几项分开写,写的到位点,如果你还学过光追那就单独写出来,如果没把握考官问你答不上来就别写给自己找麻烦,在技术栈那一栏简单提一下自己学过就行,这样杂的放在一起不太严谨,个人愚见.
点赞 评论 收藏
分享
05-09 12:23
已编辑
华南理工大学 Java
野猪不是猪🐗:给他装的,双九+有实习的能看的上这种厂我直接吃⑨✌们拿它练练面试愣是给他整出幻觉了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务