【问题标题】:Angular Unit Tests using TestBed.inject succeed/fail according to tests execution order根据测试执行顺序,使用 TestBed.inject 的 Angular 单元测试成功/失败
【发布时间】:2021-05-21 07:39:38
【问题描述】:

我有一个使用 Jest for UnitTests 的 Angular 应用程序 (v11.1.0)。

我使用TestBed.inject 来获取单个测试中的服务实例,并监视他们的测试方法,或者他们已被调用或模拟返回值。

但是切换到 Typescript strict 模式后,测试失败。但是如果我改变一些测试的顺序,一切都会顺利进行。 看起来模拟的服务仍然在不同的单元测试之间进行交互。

我尝试使用jest.resetAllMocks(),但也没有解决问题。在我使用的代码下方:

单元测试

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

  beforeEach(
    waitForAsync(() => {
      TestBed.configureTestingModule({
        imports: [
          RouterTestingModule,
          HttpClientTestingModule,
          TranslateModule.forRoot(),
        ],
        declarations: [AppComponent],
        providers: [
          { provide: InformationService, useValue: informationServiceMock }
        ],
      }).compileComponents();
    })
  );

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
  });

 describe('Test set', () => {
    it(`should have default text`, () => {
      fixture.detectChanges();
      expect(component.maintenanceBannerMessage).toBe('BANNER DE');
      expect(component.maintenanceBannerTitle).toBe('TITLE DE');
    });

    it(`Should open function`, () => {
      const dialog = TestBed.inject(MatDialog);
      const informationService = TestBed.inject(InformationService);
      jest.spyOn(informationService, 'updateOverlayAction');
      jest.spyOn(dialog, 'open');
      fixture.detectChanges();

      expect(dialog.open).toHaveBeenCalled();
      expect(informationService.updateOverlayAction).toHaveBeenCalled();
    });

    //------------------------------------------------------------
    // If I move this test in 2. position, the test `Should open function` FAILS
    // as getAnnouncementsByType keeps returning null instead of getting it from the
    // informationServiceMock provided in the configureTestingModule

    it(`should not be showed`, () => {
      const informationService = TestBed.inject(InformationService);
      jest
        .spyOn(informationService, 'getAnnouncementsByType')
        .mockReturnValue(of(null as any));
      fixture.detectChanges();

      expect(component.maintenanceBannerMessage).toBeUndefined();
      expect(component.maintenanceBannerTitle).toBeUndefined();
    });
    //------------------------------------------------------------

    it(`should not be showed`, () => {
      const dialog = TestBed.inject(MatDialog);
     const informationService = TestBed.inject(InformationService);
      jest
        .spyOn(informationService, 'getAnnouncementsByType')
        .mockReturnValue(of(null as any));
      jest.spyOn(dialog, 'open');
      fixture.detectChanges();

      expect(dialog.open).not.toHaveBeenCalled();
    });
  });
});

模拟服务

export const announcementsMockData = [
  {
    announcementId: '6000',
    type: AnnouncementType.BANNER,
    text: { de: 'BANNER DE', fr: 'BANNER FR', it: 'BANNER IT', en: 'BANNER EN' },
    title: { de: 'TITLE DE', fr: 'TITLE FR', it: 'TITLE IT', en: 'TITLE EN' }
  }, {
    announcementId: '6100',
    type: AnnouncementType.OVERLAY,
    text: { de: 'OVERLAY DE', fr: 'OVERLAY FR', it: 'OVERLAY IT', en: 'OVERLAY EN' },
    title: { de: 'OVERLAY DE', fr: 'OVERLAY FR', it: 'OVERLAY IT', en: 'OVERLAY EN' },
    _links: { create: { href: '/announcements/6100/actions' } }
  }
];

export const informationServiceMock = {
  getAnnouncementsByType: (type: AnnouncementType) => {
    return announcementsMockData.map(a => new Announcement(a)).filter(a => a.type === type);
  },
  updateOverlayAction: (link: ResourceLink, action: OverlayAction) => of(null),
};

应用组件

this.informationService.getAnnouncementsByType(AnnouncementType.BANNER)
  .pipe(takeUntil(this.destroy$))
  .subscribe(([currentLanguage, banners]) => {
    if (banners?.length > 0) {
      if (banners[0].title) {
        this.maintenanceBannerTitle = banners[0].title[currentLanguage.key as keyof LanguageObject];
      }
      if (banners[0].text) {
        this.maintenanceBannerMessage =
          banners[0].text[currentLanguage.key as keyof LanguageObject];
      }
    }
  });

【问题讨论】:

  • 尚不清楚它与 TS strict 有何关系。它不会影响它在运行时的工作方式。你是说alwaysStrict吗?
  • 我也不认为它是相关的。我在重构代码时提到了它,添加了strict 模式和 Angular strictTemplate 检查。似乎与模拟测试有关,因为颠倒了它们运行的​​顺序,因此它不应该与代码本身有关,而是与在测试中保持不变的先前模拟值有关
  • 不清楚在什么时候调用 getAnnouncementsByType。 jest.resetAllMocks() 不仅应该尝试,而且应该永久使用。行为是否与它完全相同?将其移至 beforeEach 或更好地在 Jest 配置中启用它。
  • 在 2. 测试中,我不模拟getAnnouncementsByType,因为我想测试“快乐路径”,因此 app.component 中的默认值。该方法在组件中被调用。但是,如果我在 2. 之前运行 3. 测试,getAnnouncementsByType 在组件中返回 null。好像测试会“记住”返回null 的先前模拟值。在 BeforeEach 中添加 jest.resetAllMocks() 会在 subscribe() 方法中触发组件中的异常,因为返回的 observable 是未定义的。所以我暂时把它删了。
  • 我明白了。我在没有注意到的情况下粘贴了 resetAllMocks。resetAllMocks 永远不应该在 Jest 中使用,它是有害的。请改用 restoreAllMocks 等。 好像测试会“记住”之前返回 null 的模拟值 - 确实如此,这就是为什么在每次测试之前始终将模拟恢复到原始状态很重要。

标签: angular typescript unit-testing jestjs


【解决方案1】:

应将特定于测试的间谍恢复为所有测试通用的某些实现,不这样做会导致测试交叉污染,因为测试会影响后续测试。

jest.resetAllMocks() 提供了不受欢迎的行为,应该完全避免。在beforeEach 中使用时,它会毫无例外地重置所有间谍的实现并使它们成为存根。在测试中使用时,这也会导致测试交叉污染。如果需要重置特定的间谍实现,可以使用mockReset() 来完成。

根据经验,jest.restoreAllMocks() 应该在beforeEach 中使用,它会将使用jest.spyOn 创建的所有间谍恢复到原始实现,以防万一。这种行为通常适用于所有测试,因此可以在 Jest 配置中启用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-19
    • 2011-05-04
    • 1970-01-01
    • 2020-06-04
    • 2021-10-07
    • 2012-07-12
    • 1970-01-01
    相关资源
    最近更新 更多