【问题标题】:Unit testing click event in AngularAngular中的单元测试点击事件
【发布时间】:2017-02-26 19:52:54
【问题描述】:

我正在尝试将单元测试添加到我的 Angular 2 应用程序中。在我的一个组件中,有一个带有(click) 处理程序的按钮。当用户单击按钮时,会调用在.ts 类文件中定义的函数。该函数在 console.log 窗口中打印一条消息,说明该按钮已被按下。我当前的测试代码测试打印console.log 消息:

describe('Component: ComponentToBeTested', () => {
    var component: ComponentToBeTested;

    beforeEach(() => {
        component = new ComponentToBeTested();
        spyOn(console, 'log');
    });

    it('should call onEditButtonClick() and print console.log', () => {
        component.onEditButtonClick();
        expect(console.log).toHaveBeenCalledWith('Edit button has been clicked!);
    });
});

但是,这仅测试控制器类,而不是 HTML。我不只是想测试在调用onEditButtonClick 时是否发生了日志记录;我还想测试当用户单击组件的 HTML 文件中定义的编辑按钮时调用 onEditButtonClick。我该怎么做?

【问题讨论】:

    标签: angular typescript jasmine


    【解决方案1】:

    我的目标是检查用户单击编辑按钮时是否调用了“onEditButtonClick”,而不是仅检查正在打印的 console.log。

    您需要首先使用 Angular TestBed 设置测试。这样,您实际上可以抓住按钮并单击它。你要做的是配置一个模块,就像你配置一个@NgModule,只是为了测试环境

    import { TestBed, async, ComponentFixture } from '@angular/core/testing';
    
    describe('', () => {
      let fixture: ComponentFixture<TestComponent>;
      let component: TestComponent;
    
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          imports: [ ],
          declarations: [ TestComponent ],
          providers: [  ]
        }).compileComponents().then(() => {
          fixture = TestBed.createComponent(TestComponent);
          component = fixture.componentInstance;
        });
      }));
    });
    

    那你需要窥探onEditButtonClick方法,点击按钮,检查该方法是否被调用

    it('should', async(() => {
      spyOn(component, 'onEditButtonClick');
    
      let button = fixture.debugElement.nativeElement.querySelector('button');
      button.click();
    
      fixture.whenStable().then(() => {
        expect(component.onEditButtonClick).toHaveBeenCalled();
      });
    }));
    

    这里我们需要运行一个async测试,因为按钮点击包含异步事件处理,需要通过调用fixture.whenStable()等待事件处理

    更新

    现在首选使用fakeAsync/tick 组合,而不是async/whenStable 组合。如果进行了 XHR 调用,则应使用后者,因为 fakeAsync 不支持它。所以不是上面的代码,重构,它看起来像

    it('should', fakeAsync(() => {
      spyOn(component, 'onEditButtonClick');
    
      let button = fixture.debugElement.nativeElement.querySelector('button');
      button.click();
      tick();
      expect(component.onEditButtonClick).toHaveBeenCalled();
    
    }));
    

    别忘了导入fakeAsynctick

    另见:

    【讨论】:

    • 如果我们在 html 文件中有多个按钮怎么办?做类似的事情:fixture.debugElement.nativeElement.querySelector('button');
    • 在html文件中寻找'button',但是我们有两个以上的one button呢?我应该如何引用第二个按钮进行测试?
    • 我找到了解决方案! button = fixture.debugElement.queryAll(By.css('button'); button1 = button[0];
    • 我只是花了一天时间试图弄清楚为什么这对我不起作用只是意识到我在调用我的组件元素而不是其中的 div 我实际上是在监听点击。
    • button.triggerEventHandler('click', null ) 和 button.click90 有什么区别?
    【解决方案2】:

    可以使用'@angular/core/testing' 提供的async/fakeAsync 函数测试事件,因为浏览器中的任何事件都是异步的并被推送到事件循环/队列。

    下面是一个非常基本的例子,使用fakeAsync测试点击事件。

    fakeAsync 函数通过在特殊的fakeAsync 测试区域中运行测试主体来启用线性编码样式。

    这里我正在测试一个被点击事件调用的方法。

    it('should', fakeAsync( () => {
        fixture.detectChanges();
        spyOn(componentInstance, 'method name'); //method attached to the click.
        let btn = fixture.debugElement.query(By.css('button'));
        btn.triggerEventHandler('click', null);
        tick(); // simulates the passage of time until all pending asynchronous activities finish
        fixture.detectChanges();
        expect(componentInstance.methodName).toHaveBeenCalled();
    }));
    

    下面是Angular docs 不得不说的:

    fakeAsync 优于 async 的主要优势在于测试看起来是同步的。没有then(...) 可以破坏可见的控制流。承诺返回的fixture.whenStable 不见了,取而代之的是tick()

    存在限制。例如,您不能从 fakeAsync 中拨打 XHR 电话

    【讨论】:

    • 有什么理由更喜欢这个解决方案或被接受的解决方案?作为一个有角度的菜鸟,我不知道哪个“更好”
    • @AdamHughes fakeAsync 根据角度文档更容易阅读和理解。但是你可以选择最适合你的。使用 aync 或 fakeAsync 时,没有什么比公认的解决方案更好了。
    • 我写了一个答案here,关于asyncfakeAsync 的使用之间的比较。尽管在my answer 中我使用了async,但总的来说,我更喜欢使用fakeAsync
    • 如果您的代码调用setTimeout,您应该只有tick()s,我认为不需要打勾?见stackoverflow.com/a/50574080/227299
    • 我更喜欢这个解决方案,因为它使用 debugElement(这是一个与环境无关的选择器)。
    【解决方案3】:

    我正在使用 Angular 6。我按照 Mav55 的回答,它奏效了。但是我想确定fixture.detectChanges(); 是否真的有必要,所以我删除了它,它仍然有效。然后我删除了tick(); 看看它是否有效并且确实有效。最后我从 fakeAsync() 包装中删除了测试,令人惊讶的是,它起作用了。

    所以我最终得到了这个:

    it('should call onClick method', () => {
      const onClickMock = spyOn(component, 'onClick');
      fixture.debugElement.query(By.css('button')).triggerEventHandler('click', null);
      expect(onClickMock).toHaveBeenCalled();
    });
    

    而且效果很好。

    【讨论】:

    • 我的一般策略是始终调用 fixture.detectChanges() 以触发 Angular 的生命周期事件。可能是您的组件没有ngOnInit,或者测试没有必要通过。
    • @SnailCoil 或者他可以使用自动更改检测 (angular.io/guide/testing#automatic-change-detection)
    • 一些 dom 事件是同步的。例如,input event 被记录为是同步的。如果您遇到的按钮单击事件是真的,那么这意味着按钮单击事件也是同步的。但是有很多 Dom 事件是异步的,在这种情况下您需要使用asyncfakeAsync。为了安全起见,假设所有事件都是异步的并且只使用asyncfakeAsync(这是您的偏好),我认为没有任何问题。
    • 如果您没有在完整的浏览器中运行,使用 By.css 而不是 querySelector 会更好。 (srcstackoverflow.com/questions/44400566/…)。
    • @PaulSamsotha 用tick()s 乱扔代码似乎是个坏主意,因为某些东西可能是异步的?你应该知道什么是异步的,什么不是异步的。触发鼠标/键盘/输入事件始终是同步的。
    【解决方案4】:

    我遇到了类似的问题(详细说明如下),我通过使用tick 函数解决了它(在jasmine-core: 2.52 中),其毫秒数与原始setTimeout 调用中的毫秒数相同(或更大)。

    例如,如果我有一个setTimeout(() =&gt; {...}, 2500);(因此它将在 2500 毫秒后触发),我会调用 tick(2500)这将解决问题。

    我在组件中的内容,作为对删除按钮单击的反应:

    delete() {
        this.myService.delete(this.id)
          .subscribe(
            response => {
              this.message = 'Successfully deleted! Redirecting...';
              setTimeout(() => {
                this.router.navigate(['/home']);
              }, 2500); // I wait for 2.5 seconds before redirect
            });
      }
    

    她是我的工作测试:

    it('should delete the entity', fakeAsync(() => {
        component.id = 1; // preparations..
        component.getEntity(); // this one loads up the entity to my component
        tick(); // make sure that everything that is async is resolved/completed
        expect(myService.getMyThing).toHaveBeenCalledWith(1);
        // more expects here..
        fixture.detectChanges();
        tick();
        fixture.detectChanges();
        const deleteButton = fixture.debugElement.query(By.css('.btn-danger')).nativeElement;
        deleteButton.click(); // I've clicked the button, and now the delete function is called...
    
        tick(2501); // timeout for redirect is 2500 ms :)  <-- solution
    
        expect(myService.delete).toHaveBeenCalledWith(1);
        // more expects here..
      }));
    

    附:关于fakeAsync 和测试中的一般异步的很好解释可以在这里找到:a video on Testing strategies with Angular 2 - Julie Ralph, starting from 8:10, lasting 4 minutes :)

    【讨论】:

      【解决方案5】:

      首先要检查按钮调用事件,我们需要监视单击按钮后将调用的方法 所以我们的第一行将是 spyOn 间谍方法采用两个参数 1) 组件名称 2) 成为间谍的方法,即:'onSubmit' 记住不要使用 '()' 只需要名称 然后我们需要使按钮的对象被点击 现在我们必须触发我们将添加点击事件的事件处理程序 那么我们希望我们的代码调用一次提交方法

      it('should call onSubmit method',() => {
          spyOn(component, 'onSubmit');
          let submitButton: DebugElement = 
          fixture.debugElement.query(By.css('button[type=submit]'));
          fixture.detectChanges();
          submitButton.triggerEventHandler('click',null);
          fixture.detectChanges();
          expect(component.onSubmit).toHaveBeenCalledTimes(1);
      });
      

      【讨论】:

      • 似乎对我不起作用。我正在使用 Angular 7。我认为正确的方法是:it('should call onSubmit method',() =&gt; { let mock = spyOn(component, 'onSubmit'); let submitButton: DebugElement = fixture.debugElement.query(By.css('button[type=submit]')); fixture.detectChanges(); submitButton.triggerEventHandler('click',null); fixture.detectChanges(); expect(mock).toHaveBeenCalledTimes(1); });
      猜你喜欢
      • 1970-01-01
      • 2019-11-28
      • 2020-10-10
      • 1970-01-01
      • 2017-12-15
      • 2023-04-07
      • 1970-01-01
      • 2016-07-01
      • 1970-01-01
      相关资源
      最近更新 更多