109

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?

cs95
  • 379,657
  • 97
  • 704
  • 746
Aiguo
  • 3,416
  • 7
  • 27
  • 52

5 Answers5

155

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:

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 3
    what if we have more than one button in the html file? Doing something like: fixture.debugElement.nativeElement.querySelector('button'); – Aiguo Oct 17 '16 at 20:25
  • 2
    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 Oct 17 '16 at 20:29
  • 4
    I got the solution! button = fixture.debugElement.queryAll(By.css('button'); button1 = button[0]; – Aiguo Oct 17 '16 at 21:54
  • 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? – Lukas Maurer Jun 01 '17 at 10:02
  • 3
    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. – majinnaibu Jun 03 '17 at 07:14
  • 1
    what is the difference between button.triggerEventHandler('click', null ) and button.click90? – jitenagarwal19 Feb 04 '18 at 13:25
  • Perfect solution. :) – Akhil Nair Jul 20 '18 at 12:16
  • 1
    This Worked for me, id of button prefixing as button#, let btn = fixture.debugElement.nativeElement.querySelector("button#button_id"); – Codiee Sep 27 '18 at 12:35
  • 1
    How will you write test cases for a row click in a table? – Arj 1411 Jun 21 '19 at 07:07
  • Whatever I write inside whenStable it passes wrong assert as well, even if you write expect(false).toBeTruthy(); it will pass this assert. – Mohini Mhetre Dec 11 '19 at 12:00
  • 1
    @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. – Paul Samsotha Dec 12 '19 at 18:50
  • @PaulSamsotha yes i wrote that in async but till it was passing wrong assert. I ended using fakeAsync and tick – Mohini Mhetre Dec 17 '19 at 08:31
  • Thanks, btw I am getting this error "Expected spy onSubmit to have been called.", what am I doing wrong? – Neel Apr 19 '20 at 12:35
  • 1
    Another way to select the button : const button = debugElement.query(By.css('#buttonId')).nativeElement; – Jonath P Sep 13 '21 at 08:54
58

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

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
Mav55
  • 4,080
  • 2
  • 21
  • 20
  • Is there any reason to prefer this solution or the accepted solution? As an angular noob, I have no idea which is "better" – Adam Hughes Nov 06 '17 at 19:41
  • 2
    @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. – Mav55 Nov 07 '17 at 22:22
  • 1
    I wrote an answer [here](https://stackoverflow.com/a/42972411/2587435) about the comparison between the use of `async` vs. `fakeAsync`. Even though in [my answer](https://stackoverflow.com/a/40093298/2587435) I used `async`, in general, my preference would be to use `fakeAsync`. – Paul Samsotha Dec 11 '18 at 22:27
  • 1
    You should only have `tick()`s if your code is calling `setTimeout`, I don't think a tick is needed? See https://stackoverflow.com/a/50574080/227299 – Ruan Mendes Nov 20 '19 at 13:16
  • I prefer this solution as it uses debugElement (Which is an environment agnostic selector). – JoshuaTree Jan 05 '21 at 07:57
  • [tick](https://angular.io/api/core/testing/tick) is explicitly used for simulating the passage of time. In most cases, `detctChanges` is enough. – David S. Jan 15 '23 at 02:29
13

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.

Parziphal
  • 6,222
  • 4
  • 34
  • 36
  • 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. – SnailCoil Jul 19 '18 at 23:42
  • @SnailCoil or he could use automatic change detection (https://angular.io/guide/testing#automatic-change-detection) – Maksymilian Majer Aug 02 '18 at 13:47
  • 1
    Some dom events are synchronous. For example the [input event](https://developer.mozilla.org/en-US/docs/Web/Events/input) 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 Dec 11 '18 at 22:24
  • In case you are not running in a complete browser, using By.css instead of querySelector is better. (src https://stackoverflow.com/questions/44400566/angular-4-unit-test-de-queryby-css-versus-native-web-api-of-de-nativeele). – Ambroise Rabier Jul 06 '19 at 15:18
  • 1
    @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. – Ruan Mendes Nov 20 '19 at 13:20
  • the link to automatic change detection Maksymilian posted has moved to https://angular.io/guide/testing-components-scenarios#automatic-change-detection – bobbyg603 Mar 05 '23 at 22:46
2

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);
});
Afzal Azad
  • 31
  • 2
  • 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); }); ``` – Todor Todorov Oct 30 '19 at 12:06
1

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 :)

Aleksandar
  • 3,558
  • 1
  • 39
  • 42