【问题标题】:.detectChanges() not working within Angular test.detectChanges() 在 Angular 测试中不起作用
【发布时间】:2023-04-03 10:00:01
【问题描述】:

我的任务是为使用 Angular 开发的聊天应用程序编写测试。以下是我目前正在为其编写测试的 Angular 模板代码的 sn-p:

<div class="title-menu-container" fxLayoutAlign="center center">
  <button id="save-title-button" mat-icon-button *ngIf="titleInputEdit; else settings">
    <mat-icon class="secondary-text" (click)="saveTitle(titleInput.value)">check</mat-icon>
  </button>
  <ng-template #settings>
    <button mat-icon-button [matMenuTriggerFor]="menu" [disabled]="!(isGroupConversation$ | async)">
      <mat-icon class="secondary-text">settings</mat-icon>
    </button>
  </ng-template>
</div>

本质上,如果组件布尔变量'titleInputEdit'为真,则显示保存标题按钮,否则显示设置按钮。这是导致问题的测试用例:

it('save title button should be present', () => {
  component.titleInputEdit = true;
  fixture.detectChanges();
  expect(fixture.nativeElement.querySelector('#save-title-button')).not.toBe(null);
}); 

我只是“模拟”组件变量,调用 .detectChanges(),然后测试按钮是否存在。但是,测试失败并显示“预期 null 不为 null”。

通过各种 console.log 调用,我确认 component.titleInputEdit 已正确设置为 true,但 fixture.nativeElement 不包含正确的按钮。

我注意到的一些事情:

  • 如果我将 'component.titleInputEdit = true' 行移到我的 beforeEach 中并将其删除,并且从我的测试中调用 detectChanges(),则测试通过。

    beforeEach(() => {
      fixture = TestBed.createComponent(TestComponent);
      component = fixture.componentInstance;
      component.titleInputEdit = true
      fixture.detectChanges();
      debugElement = fixture.debugElement;
    });     
    
    it('save title button should be present', () => {
        expect(fixture.nativeElement.querySelector('#save-title-button')).not.toBe(null);
    });
    
  • 如果我从 beforeEach() 中删除 .detectChanges() 调用,并将其留在测试用例中,则测试通过。

我对 Angular 比较陌生,但我发现有人遇到类似问题。在尝试了这些帖子中推荐的一些东西之后,我仍然摸不着头脑。更奇怪的是,我已经为其他 Angular 组件编写了测试,这些组件几乎可以毫无问题地做同样的事情。

Angular 文档中提供的示例也显示了非常相似的内容:

it('should display a different test title', () => {
  component.title = 'Test Title';
  fixture.detectChanges();
  expect(h1.textContent).toContain('Test Title');
});    

【问题讨论】:

标签: angular testing jasmine


【解决方案1】:

原来这是由于在组件中使用了ChangeDetectionStrategy.OnPush。使用OnPush 只允许您调用.detectChanges() 一次,因此后续调用将无法执行任何操作。我对 Angular 不够熟悉,无法完全理解原因。

通过覆盖我的 TestBed 配置中的 ChangeDetectionStrategy,我能够产生所需的行为。

TestBed.configureTestingModule({
    imports: [],
    declarations: [TestComponent],
    providers: []
  })
    .overrideComponent(TestComponent, {
      set: { changeDetection: ChangeDetectionStrategy.Default }
    })
    .compileComponents();

【讨论】:

  • 对不起,答案有点误导。 OnPush 策略告诉仅在其父组件更新组件输入之一时标记您的组件以进行检查。 Angular 无法跟踪在测试中手动设置属性,因此在 DOM 中看不到任何更改。如果titleInputEdit 是输入,那么在测试中创建一个主机组件并将其属性绑定到titleInputEdit。只是 Using OnPush only allows you to call .detectChanges() one time 不是 100% 正确的声明。在此之前应该标记您的组件以供检查。
  • 当您使用onPush 策略时,您可能在组件中有一个cdr 属性,因此您可以使用它(它会起作用)component.cdr.detectChanges()
【解决方案2】:

至今仍在运行...

我个人喜欢最重要的 ChangeDetectionStrategy solution,因为它在 TestBed 设置中是一次性的,但我知道这种侵入式解决方案并不理想。

TestBed.configureTestingModule({
    imports: [],
    declarations: [TestComponent],
    providers: []
})
.overrideComponent(TestComponent, {
    set: { changeDetection: ChangeDetectionStrategy.Default }
})
.compileComponents();

我看到有“ChangeDetectorRef”解决方案在组件类本身上使用“changeDetector.markForCheck()”,这不是一个好方法,因为您的组件不必适应测试,但您可以仍然使用此解决方案,而不会弄乱实际组件,而是通过调用而不是正常的“detectChanges()”,如 here

所示
const cdr = debugEl.injector.get<ChangeDetectorRef>(ChangeDetectorRef as any);
cdr.detectChanges();

最后是最简单的解决方案,至少在我的脑海中,但奇怪的是,我还没有找到任何提及它的内容。 所以,你可能已经知道你可以(或最终不得不)创建一个主机组件来包装你正在测试的那个,很多博客,for example,在那里展示了@ViewChild(ComponentUnderTestComponent) 方法的使用,这将如果 jasmine 可以真正感知到子组件的变化,那就完美了,但是,看起来,它并没有,我们坚持使用通常的直观方法,即在主机中列出输入并将它们直接绑定到模板中测试组件,如下所示:

@Component({
    template: `<component-tag [(ngModel)]="val" [someProperty]="flag"></component-tag>`
})
class HostComponent {
    val: number;
    flag: boolean = false;
}

这样,现在您实际上可以更改 HostComponent.someProperty 的值,然后调用 detectChanges() 并且 jasmine 将完美地完成它应该做的事情并使用更改更新 DOM:

fixture.componentInstance.readonly = true;
fixture.detectChanges();

现在,如果你的组件继续运行并且有几十个输入属性,那么我想这不是真的可行,但无论如何,我想我会把它扔在那里,享受吧

【讨论】:

    【解决方案3】:

    这里提供的答案https://stackoverflow.com/a/50142134/3765819 解决了这个问题。但是,我认为还有另一种方法可以防止 UI 出现进一步的问题。我遇到的问题与问题中描述的问题相似,这意味着在 HTML 上测试特定字符串时我找不到它。即使在运行代码时它的措辞很好,但在测试时 UI 并没有相应地更新。

    我必须做的是:

    ChangeDetectorRef 注入.ts 文件:

    constructor(private changeDetector: ChangeDetectorRef) {}
    

    并在需要时调用它:

    this.changeDetector.markForCheck();
    

    【讨论】:

    • 仅仅为了方便测试而改变组件是个坏主意
    【解决方案4】:

    在我的情况下,由于异步加载,我需要使用 fixture.whenStable 而不仅仅是 fixture.detectChanges,例如

    it('test description', async(async () => {
    
        await fixture.whenStable();
    }));
    

    【讨论】:

      【解决方案5】:

      更好的方法是使用ChangeDetectionStrategy.Default策略在规范本身中编写一个包装器组件,并通过父组件实例(即包装器组件)在规范中测试要测试的实际组件(即子组件) .

      子实例及其本机元素可以通过父级的fixture.debugElement 使用fixture.debugElement.query(By.css('your-child-selector')) 访问

      【讨论】:

        【解决方案6】:

        我知道这个问题很老了,但我最近遇到了同样的问题,即微调器会在 Karma 页面上不断旋转,因为更改检测只发生了一次。我的解决方法是调用 fixture.detectChanges(true) 还是 fixture.autoDetectChanges(true)。

        beforeEach(() => { 
          fixture = TestBed.createComponent(TestComponent);
          component = fixture.componentInstance;
          component.titleInputEdit = true
          // 'detectChanges' will only test for onPush events: 
          // fixture.detectChanges();
        
          // 'autoDetectChanges' will continually check for changes until the test is complete.  
          // This is slower, but necessary for certain UI changes
          fixture.autoDetectChanges(true);
        
          debugElement = fixture.debugElement;
        }); 
        

        【讨论】:

        • 这有点把婴儿和洗澡水一起扔了。您有充分的理由要使用手动 detectChanges 策略,所以简单地扔掉所有这些并不是正确的解决方案
        猜你喜欢
        • 2018-06-13
        • 2022-11-02
        • 2019-02-17
        • 2020-11-20
        • 1970-01-01
        • 1970-01-01
        • 2020-08-21
        • 2016-05-14
        • 2014-03-18
        相关资源
        最近更新 更多