I have a fairly typical, simple ng2 component that calls a service to get some data (carousel items). It also uses setInterval to auto-switch carousel slides in the UI every n seconds. It works just fine, but when running Jasmine tests I get the error: "Cannot use setInterval from within an async test zone".
I tried wrapping the setInterval call in this.zone.runOutsideAngular(() => {...}), but the error remained. I would've thought changing the test to run in fakeAsync zone would solve the problem, but then I get an error saying XHR calls are not allowed from within fakeAsync test zone (which does make sense).
How can I use both the XHR calls made by the service and the interval, while still being able to test the component? I'm using ng2 rc4, project generated by angular-cli. Many thanks in advance.
My code from the component:
constructor(private carouselService: CarouselService) {
}
ngOnInit() {
this.carouselService.getItems().subscribe(items => {
this.items = items;
});
this.interval = setInterval(() => {
this.forward();
}, this.intervalMs);
}
And from the Jasmine spec:
it('should display carousel items', async(() => {
testComponentBuilder
.overrideProviders(CarouselComponent, [provide(CarouselService, { useClass: CarouselServiceMock })])
.createAsync(CarouselComponent).then((fixture: ComponentFixture<CarouselComponent>) => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
// some expectations here;
});
}));
Clean code is testable code. setInterval is sometimes difficult to test because the timing is never perfect. You should abstract the setTimeout into a service that you can mock out for the test. In the mock you can have controls to handle each tick of the interval. For example
class IntervalService {
interval;
setInterval(time: number, callback: () => void) {
this.interval = setInterval(callback, time);
}
clearInterval() {
clearInterval(this.interval);
}
}
class MockIntervalService {
callback;
clearInterval = jasmine.createSpy('clearInterval');
setInterval(time: number, callback: () => void): any {
this.callback = callback;
return null;
}
tick() {
this.callback();
}
}
With the MockIntervalService you can now control each tick, which is so much more easy to reason about during testing. There's also a spy to check that the clearInterval method is called when the component is destroyed.
For your CarouselService, since it is also asynchronous, please see this post for a good solution.
Below is a complete example (using RC 6) using the previously mentioned services.
import { Component, OnInit, OnDestroy } from '#angular/core';
import { CommonModule } from '#angular/common';
import { TestBed } from '#angular/core/testing';
class IntervalService {
interval;
setInterval(time: number, callback: () => void) {
this.interval = setInterval(callback, time);
}
clearInterval() {
clearInterval(this.interval);
}
}
class MockIntervalService {
callback;
clearInterval = jasmine.createSpy('clearInterval');
setInterval(time: number, callback: () => void): any {
this.callback = callback;
return null;
}
tick() {
this.callback();
}
}
#Component({
template: '<span *ngIf="value">{{ value }}</span>',
})
class TestComponent implements OnInit, OnDestroy {
value;
constructor(private _intervalService: IntervalService) {}
ngOnInit() {
let counter = 0;
this._intervalService.setInterval(1000, () => {
this.value = ++counter;
});
}
ngOnDestroy() {
this._intervalService.clearInterval();
}
}
describe('component: TestComponent', () => {
let mockIntervalService: MockIntervalService;
beforeEach(() => {
mockIntervalService = new MockIntervalService();
TestBed.configureTestingModule({
imports: [ CommonModule ],
declarations: [ TestComponent ],
providers: [
{ provide: IntervalService, useValue: mockIntervalService }
]
});
});
it('should set the value on each tick', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
let el = fixture.debugElement.nativeElement;
expect(el.querySelector('span')).toBeNull();
mockIntervalService.tick();
fixture.detectChanges();
expect(el.innerHTML).toContain('1');
mockIntervalService.tick();
fixture.detectChanges();
expect(el.innerHTML).toContain('2');
});
it('should clear the interval when component is destroyed', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
fixture.destroy();
expect(mockIntervalService.clearInterval).toHaveBeenCalled();
});
});
I had the same problem: specifically, getting this errror when a third party service was calling setInterval() from a test:
Error: Cannot use setInterval from within an async zone test.
You can mock out the calls, but that is not always desirable, since you may actually want to test the interaction with another module.
I solved it in my case by just using Jasmine's (>=2.0) async support instead of Angulars's async():
it('Test MyAsyncService', (done) => {
var myService = new MyAsyncService()
myService.find().timeout(1000).toPromise() // find() returns Observable.
.then((m: any) => { console.warn(m); done(); })
.catch((e: any) => { console.warn('An error occured: ' + e); done(); })
console.warn("End of test.")
});
What about using the Observable? https://github.com/angular/angular/issues/6539
To test them you should use the .toPromise() method
Related
I want to test my service in angular with karma and jasmine, I begin with unit tests and I don't found any solution for my case or because I don't know how I can fix the issue. I always have a problem with my fields which are often undefined I don't understand why. If you can explain me what is the real problem It will be very nice to be able to progress in my tests.
My service
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class CanvasElementService {
public canvasContainer: HTMLDivElement;
private scaleProperty: string = 'scale(1)';
public updateScale(scale: number) {
this.scaleProperty = `scale(${scale})`;
this.update();
}
public update(): void {
this.canvasContainer.style.transform = this.scaleProperty;
}
}
And my test
import { Component } from '#angular/core';
import { TestBed } from '#angular/core/testing';
import { CanvasElementService } from './canvas-element.service';
fdescribe('CanvasElementService', () => {
let service: CanvasElementService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CanvasElementService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe("updateScale", () => {
it("updatescale 2", () => {
let scale = 3;
spyOn(service, 'update').and.callThrough();
service.updateScale(scale);
expect(service['scaleProperty']).toBe('scale(3)');
});
});
});
After run test I got :
thanks for help If you have any idea to fix it.
Im not quite sure how to access a htmlElement in services. I think its only meant to be used by components (which have access to elements via #ViewChild() decorator).
You can try to achieve it by injecting your document into the service constructor and search for your element. Something like this:
constructor(#Inject(DOCUMENT) private document: HTMLDocument) {}
But its really not recommend to use services to access your dom. Use components or directives for it.
https://medium.com/#smarth55/angular-bad-practices-patterns-service-that-touch-the-dom-57ea978dca92
I'm working on angular app.
In my service file, I created a function configure. And it as been called in AfterViewInit in an component.
But On load time, this.config is undefined, If I use it inside setTimeOut I could able to access the value of the this.config.
The below code works,
configure() {
setTimeout(() => {
if(this.config) {
this.apply();
}
}, 200);
}
Is there any better way to do it ? without using setTimeOut.
Please help
try with markForCheck method
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(){
{
this.cdr.markForCheck();
this.configure()
}
configure(){
if(this.config) {
// your code
}
}
add the below code to #Component decorator
changeDetection: ChangeDetectionStrategy.OnPush,
Can you please try with below code
configure() {
var self = this;
setTimeout(() => {
if(self.config) {
self.apply();
}
}, 200);
}
I am trying to share data between components using the rxjs subject and i've used that data in component
Component.html
<div class="spinner-container" *ngIf="loading">
<div class="spinner-item">
<nx-spinner nxSize="large"></nx-spinner>
</div>
</div>
component.ts
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
})
},100)
}
Here is the error
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed
after it was checked. Previous value: 'ngIf: false'. Current value:
'ngIf: true'.
I found a workaround using changedetectref but I don't think its good practice is ther any other way to solve this issue
You can manually trigger change detection using the detectChanges() method of the ChangeDetectorRef
Try like this:
import { ChangeDetectorRef} from '#angular/core';
constructor(private cdr: ChangeDetectorRef) { }
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
if (this.cdr && !(this.cdr as ViewRef).destroyed) {
this.cdr.detectChanges();
}
})
},100)
}
Making the next callback async worked for me once:
this.commonService.spinnerTrigger.subscribe(async (trigger) => {
this.loading = await trigger;
});
Or adding a zero delay:
this.commonService.spinnerTrigger.pipe(delay(0)).subscribe((trigger) => {
this.loading = trigger;
});
This is an open issue in Github,
Github issue => https://github.com/angular/angular/issues/15634
And they provided a workaround using setTimeout() for now and still there aren't any updates regarding this issue.
And also you can try changeDetector that may solve your issue.
import { ChangeDetectorRef } from '#angular/core';
constructor(private cdRef:ChangeDetectorRef) {}
ngAfterViewChecked()
{
this.cdRef.detectChanges();
}
I don't see any need here to mess around with change detection / setTimeout (which triggers change detection).
Stackblitz
Use a spinner service which parent and child can use.
spinner.service.ts
#Injectable()
export class SpinnerService {
private loading = new BehaviorSubject<boolean>(true)
loading$: Observable<boolean> = this.loading.asObservable()
setSpinner(bool: boolean) {
this.loading.next(bool)
}
}
Example - Component setting spinner
ngOnInit() {
this.service.getChildData().pipe(
// handle any errors
catchError(err => {
console.log('Error caught: ', err)
this.data = err
return throwError(err)
}),
// no matter what set spinner false
finalize(() => {
this.spinnerService.setSpinner(false)
}),
// subscription clean up
takeUntil(this.destroyed$)
).subscribe(data => this.data = data)
}
Example - parent / container displaying spinner
ngOnInit() {
this.loading$ = this.spinnerService.loading$
this.spinnerService.setSpinner(true) // if needed
}
<div *ngIf="loading$ | async">
I am a spinner
</div>
I would like to ask if can be tested calling the right function dependent on the condition with sinon or mocha. For example I have class Knight and I want to know if a function (knightRun) is called, when parameter 'data' is true.
export class Knight {
createKnight(data,reducer) {
if (data) {
this.knightRun(reducer);
} else if (!data) {
this.knightFight(reducer);
}
}
private knightFight(reducer) {
// do something
}
private knightRun(reducer) {
// do something
}
}
You can use spies to check whether a particular function has been called. Sinon.js is a library which provides a way to spy on functions when writing unit tests for your JavaScript.
e.g.
describe('Knight class', () => {
it('should call knightRun when data is false', () => {
const knight = new Knight().createKnight(false, null)
sinon.spy(knight, "knightRun")
assert(knight.knightRun.calledOnce)
})
it('should call knightFight when data is true', () => {
const knight = new Knight().createKnight(true, null)
sinon.spy(knight, "knightFight")
assert(knight.knightFight.calledOnce)
})
})
As an aside, the private keyword is not valid JavaScript.
According to the Jasmine documentation, a mock can be created like this:
jasmine.createSpyObj(someObject, ['method1', 'method2', ... ]);
How do you stub one of these methods? For example, if you want to test what happens when a method throws an exception, how would you do that?
You have to chain method1, method2 as EricG commented, but not with andCallThrough() (or and.callThrough() in version 2.0). It will delegate to real implementation.
In this case you need to chain with and.callFake() and pass the function you want to be called (can throw exception or whatever you want):
var someObject = jasmine.createSpyObj('someObject', [ 'method1', 'method2' ]);
someObject.method1.and.callFake(function() {
throw 'an-exception';
});
And then you can verify:
expect(yourFncCallingMethod1).toThrow('an-exception');
If you are using Typescript, it's helpful to cast the method as Jasmine.Spy. In the above Answer (oddly I don't have rep for comment):
(someObject.method1 as Jasmine.Spy).and.callFake(function() {
throw 'an-exception';
});
I don't know if I'm over-engineering, because I lack the knowledge...
For Typescript, I want:
Intellisense from the underlying type
The ability to mock just the methods used in a function
I've found this useful:
namespace Services {
class LogService {
info(message: string, ...optionalParams: any[]) {
if (optionalParams && optionalParams.length > 0) {
console.log(message, optionalParams);
return;
}
console.log(message);
}
}
}
class ExampleSystemUnderTest {
constructor(private log: Services.LogService) {
}
doIt() {
this.log.info('done');
}
}
// I export this in a common test file
// with other utils that all tests import
const asSpy = f => <jasmine.Spy>f;
describe('SomeTest', () => {
let log: Services.LogService;
let sut: ExampleSystemUnderTest;
// ARRANGE
beforeEach(() => {
log = jasmine.createSpyObj('log', ['info', 'error']);
sut = new ExampleSystemUnderTest(log);
});
it('should do', () => {
// ACT
sut.doIt();
// ASSERT
expect(asSpy(log.error)).not.toHaveBeenCalled();
expect(asSpy(log.info)).toHaveBeenCalledTimes(1);
expect(asSpy(log.info).calls.allArgs()).toEqual([
['done']
]);
});
});
Angular 9
Using jasmine.createSpyObj is ideal when testing a component where a simple service is injected. For example: let's say, in my HomeComponent I have a HomeService (injected). The only method in the HomeService is getAddress().
When creating the HomeComponent test suite, I can initialize the component and service as:
describe('Home Component', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
let element: DebugElement;
let homeServiceSpy: any;
let homeService: any;
beforeEach(async(() => {
homeServiceSpy = jasmine.createSpyObj('HomeService', ['getAddress']);
TestBed.configureTestingModule({
declarations: [HomeComponent],
providers: [{ provide: HomeService, useValue: homeServiceSpy }]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
homeService = TestBed.get(HomeService);
fixture.detectChanges();
});
}));
it('should be created', () => {
expect(component).toBeTruthy();
});
it("should display home address", () => {
homeService.getAddress.and.returnValue(of('1221 Hub Street'));
fixture.detectChanges();
const address = element.queryAll(By.css(".address"));
expect(address[0].nativeNode.innerText).toEqual('1221 Hub Street');
});
});
This is a simple way to test your component using jasmine.createSpyObj. However, if your service has more methods more complex logic, I would recommend creating a mockService instead of createSpyObj. For example:
providers: [{ provide: HomeService, useValue: MockHomeService }]
Hope this helps!
Building on #Eric Swanson's answer, I've created a better readable and documented function for using in my tests. I also added some type safety by typing the parameter as a function.
I would recommend to place this code somewhere in a common test class, so that you can import it in every test file that needs it.
/**
* Transforms the given method into a jasmine spy so that jasmine functions
* can be called on this method without Typescript throwing an error
*
* #example
* `asSpy(translator.getDefaultLang).and.returnValue(null);`
* is equal to
* `(translator.getDefaultLang as jasmine.Spy).and.returnValue(null);`
*
* This function will be mostly used in combination with `jasmine.createSpyObj`, when you want
* to add custom behavior to a by jasmine created method
* #example
* `const translator: TranslateService = jasmine.createSpyObj('TranslateService', ['getDefaultLang'])
* asSpy(translator.getDefaultLang).and.returnValue(null);`
*
* #param {() => any} method - The method that should be types as a jasmine Spy
* #returns {jasmine.Spy} - The newly typed method
*/
export function asSpy(method: () => any): jasmine.Spy {
return method as jasmine.Spy;
}
Usage would be as follows:
import {asSpy} from "location/to/the/method";
const translator: TranslateService = jasmine.createSpyObj('TranslateService', ['getDefaultLang']);
asSpy(translator.getDefaultLang).and.returnValue(null);