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?
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()
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
.
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-returningfixture.whenStable
is gone, replaced bytick()
There are limitations. For example, you cannot make an XHR call from within a
fakeAsync
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.
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 :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With