Unit testing click event in Angular

196,026

Solution 1

My objective is to check if the 'onEditButtonClick' is getting invoked when the user clicks the edit button and not checking just the console.log being printed.

You will need to first set up the test using the Angular TestBed. This way you can actually grab the button and click it. What you will do is configure a module, just like you would an @NgModule, just for the testing environment

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

Then you need to spy on the onEditButtonClick method, click the button, and check that the method was called

it('should', async(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();

  fixture.whenStable().then(() => {
    expect(component.onEditButtonClick).toHaveBeenCalled();
  });
}));

Here we need to run an async test as the button click contains asynchronous event handling, and need to wait for the event to process by calling fixture.whenStable()

Update

It is now preferred to use fakeAsync/tick combo as opposed to the async/whenStable combo. The latter should be used if there is an XHR call made, as fakeAsync does not support it. So instead of the above code, refactored, it would look like

it('should', fakeAsync(() => {
  spyOn(component, 'onEditButtonClick');

  let button = fixture.debugElement.nativeElement.querySelector('button');
  button.click();
  tick();
  expect(component.onEditButtonClick).toHaveBeenCalled();

}));

Don't forget to import fakeAsync and tick.

See also:

Solution 2

Events can be tested using the async/fakeAsync functions provided by '@angular/core/testing', since any event in the browser is asynchronous and pushed to the event loop/queue.

Below is a very basic example to test the click event using fakeAsync.

The fakeAsync function enables a linear coding style by running the test body in a special fakeAsync test zone.

Here I am testing a method that is invoked by the click event.

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

Below is what Angular docs have to say:

The principle advantage of fakeAsync over async is that the test appears to be synchronous. There is no then(...) to disrupt the visible flow of control. The promise-returning fixture.whenStable is gone, replaced by tick()

There are limitations. For example, you cannot make an XHR call from within a fakeAsync

Solution 3

I'm using Angular 6. I followed Mav55's answer and it worked. However I wanted to make sure if fixture.detectChanges(); was really necessary so I removed it and it still worked. Then I removed tick(); to see if it worked and it did. Finally I removed the test from the fakeAsync() wrap, and surprise, it worked.

So I ended up with this:

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

And it worked just fine.

Solution 4

to check button call event first we need to spy on method which will be called after button click so our first line will be spyOn spy methode take two arguments 1) component name 2) method to be spy i.e: 'onSubmit' remember not use '()' only name required then we need to make object of button to be clicked now we have to trigger the event handler on which we will add click event then we expect our code to call the submit method once

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

Solution 5

I had a similar problem (detailed explanation below), and I solved it (in jasmine-core: 2.52) by using the tick function with the same (or greater) amount of milliseconds as in original setTimeout call.

For example, if I had a setTimeout(() => {...}, 2500); (so it will trigger after 2500 ms), I would call tick(2500), and that would solve the problem.

What I had in my component, as a reaction on a Delete button click:

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

Her is my working test:

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..
  }));

P.S. Great explanation on fakeAsync and general asyncs in testing can be found here: a video on Testing strategies with Angular 2 - Julie Ralph, starting from 8:10, lasting 4 minutes :)

Share:
196,026
Aiguo
Author by

Aiguo

Updated on January 08, 2021

Comments

  • Aiguo
    Aiguo over 3 years

    I'm trying to add unit tests to my Angular 2 app. In one of my components, there is a button with a (click) handler. When the user clicks the button a function is called which is defined in the .ts class file. That function prints a message in the console.log window saying that the button has been pressed. My current testing code tests for printing of the console.log message:

    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!);
        });
    });
    

    However, this only tests the controller class, not the HTML. I don't just want to test that the logging happens when onEditButtonClick is called; I also want to test that onEditButtonClick is called when the user clicks the edit button defined in the component's HTML file. How can I do that?

  • Aiguo
    Aiguo over 7 years
    what if we have more than one button in the html file? Doing something like: fixture.debugElement.nativeElement.querySelector('button');
  • Aiguo
    Aiguo over 7 years
    looks for 'button' in the html file, but what we have more than two one button? How should I reference the second button occurrence for testing?
  • Aiguo
    Aiguo over 7 years
    I got the solution! button = fixture.debugElement.queryAll(By.css('button'); button1 = button[0];
  • nuke
    nuke about 7 years
    Thanks for the answer. I noticed that you omitted the import for IonicModule.forRoot, though. For me the test fails once I add that. Isn't it required for other testing scenarios?
  • majinnaibu
    majinnaibu about 7 years
    I just spent a day trying to figure out why this wasn't working for me only to realize I was calling click on my component's element not the div inside it I'm actually listening for clicks on.
  • Adam Hughes
    Adam Hughes over 6 years
    Is there any reason to prefer this solution or the accepted solution? As an angular noob, I have no idea which is "better"
  • Mav55
    Mav55 over 6 years
    @AdamHughes fakeAsync is more easier to read and understand according to angular documentation. But you can choose whatever suits you best. There is nothing like accepted solution when using aync or fakeAsync.
  • jitenagarwal19
    jitenagarwal19 over 6 years
    what is the difference between button.triggerEventHandler('click', null ) and button.click90?
  • SnailCoil
    SnailCoil almost 6 years
    my general strategy has been to always invoke fixture.detectChanges() in order to trigger angular's lifecycle events. it may have been the case that your component doesn't have an ngOnInit or that it wasn't necessary for the test to pass.
  • Akhil Nair
    Akhil Nair almost 6 years
    Perfect solution. :)
  • Maksymilian Majer
    Maksymilian Majer almost 6 years
    @SnailCoil or he could use automatic change detection (angular.io/guide/testing#automatic-change-detection)
  • Codiee
    Codiee almost 6 years
    This Worked for me, id of button prefixing as button#, let btn = fixture.debugElement.nativeElement.querySelector("button#but‌​ton_id");
  • Paul Samsotha
    Paul Samsotha over 5 years
    Some dom events are synchronous. For example the input event is documented to be synchronous. If you what are experiencing with the button click event is true, then that means that the button click event is also synchronous. But there are a lot of Dom events that are asynchronous, in which case you would need to use async or fakeAsync. To be safe, I don't see anything wrong with assuming that all events are asynchronous and just use async or fakeAsync (it's your preference).
  • Paul Samsotha
    Paul Samsotha over 5 years
    I wrote an answer here about the comparison between the use of async vs. fakeAsync. Even though in my answer I used async, in general, my preference would be to use fakeAsync.
  • Arj 1411
    Arj 1411 about 5 years
    How will you write test cases for a row click in a table?
  • Ambroise Rabier
    Ambroise Rabier about 5 years
    In case you are not running in a complete browser, using By.css instead of querySelector is better. (src stackoverflow.com/questions/44400566/…).
  • Todor Todorov
    Todor Todorov over 4 years
    Does not seem to work for me. I am using Angular 7. I think the right approach is: it('should call onSubmit method',() => { 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); });
  • Ruan Mendes
    Ruan Mendes over 4 years
    You should only have tick()s if your code is calling setTimeout, I don't think a tick is needed? See stackoverflow.com/a/50574080/227299
  • Ruan Mendes
    Ruan Mendes over 4 years
    @PaulSamsotha Seems like a bad idea to litter your code with tick()s because something may potentially be async? You should know what is and isn't async. Triggering mouse/keyboard/input events is always synchronous.
  • Mohini Mhetre
    Mohini Mhetre over 4 years
    Whatever I write inside whenStable it passes wrong assert as well, even if you write expect(false).toBeTruthy(); it will pass this assert.
  • Paul Samsotha
    Paul Samsotha over 4 years
    @MohiniMhetre do you have the callback wrapped in async? Anyway, it is advises to use fakeAsync now instead of async. Use async only when there is an XHR call.
  • Mohini Mhetre
    Mohini Mhetre over 4 years
    @PaulSamsotha yes i wrote that in async but till it was passing wrong assert. I ended using fakeAsync and tick
  • Neel
    Neel about 4 years
    Thanks, btw I am getting this error "Expected spy onSubmit to have been called.", what am I doing wrong?
  • JoshuaTree
    JoshuaTree over 3 years
    I prefer this solution as it uses debugElement (Which is an environment agnostic selector).
  • Jonath P
    Jonath P almost 3 years
    Another way to select the button : const button = debugElement.query(By.css('#buttonId')).nativeElement;