Unit Test RxJS Observable.timer using typescript, karma and jasmine - unit-testing

Hi I'm relatively new to Angular2, Karma and Jasmine. Currently I'm using Angular 2 RC4 Jasmine 2.4.x
I have an Angular 2 service which periodically calls an http service like this:
getDataFromDb() { return Observable.timer(0, 2000).flatMap(() => {
return this.http.get(this.backendUrl)
.map(this.extractData)
.catch(this.handleError);
});
}
Now I want to test the functionality. For testing purposes I have just tested the "http.get" on a separate function without the Observable.timer by doing:
const mockHttpProvider = {
deps: [MockBackend, BaseRequestOptions],
useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => {
return new Http(backend, defaultOptions);
}
}
describe('data.service test suite', () => {
var dataFromDbExpected: any;
beforeEachProviders(() => {
return [
DataService,
MockBackend,
BaseRequestOptions,
provide(Http, mockHttpProvider),
];
});
it('http call to obtain data',
inject(
[DataService, MockBackend],
fakeAsync((service: DataService, backend: MockBackend) => {
backend.connections.subscribe((connection: MockConnection) => {
dataFromDbExpected = 'myData';
let mockResponseBody: any = 'myData';
let response = new ResponseOptions({ body: mockResponseBody });
connection.mockRespond(new Response(response));
});
const parsedData$ = service.getDataFromDb()
.subscribe(response => {
console.log(response);
expect(response).toEqual(dataFromDbExpected);
});
})));
});
I obviously want to test the whole function with the Observable.timer. I think one might want to use the TestScheduler from the rxjs framework, but how can I tell to only repeat the timer function for x times? I couln't find any documentation using it in the typescript context.
Edit: I'm using rxjs 5 beta 6
Edit: Added working example for Angular 2.0.0 final release:
describe('when getData', () => {
let backend: MockBackend;
let service: MyService;
let fakeData: MyData[];
let response: Response;
let scheduler: TestScheduler;
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
backend = be;
service = new MyService(http);
fakeData = [{myfake: 'data'}];
let options = new ResponseOptions({ status: 200, body: fakeData });
response = new Response(options);
scheduler = new TestScheduler((a, b) => expect(a).toEqual(b));
const originalTimer = Observable.timer;
spyOn(Observable, 'timer').and.callFake(function (initialDelay, dueTime) {
return originalTimer.call(this, initialDelay, dueTime, scheduler);
});
}));
it('Should do myTest', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
scheduler.schedule(() => {
service.getMyData().subscribe(
myData => {
expect(myData.length).toBe(3,
'should have expected ...');
});
}, 2000, null);
scheduler.flush();
})));
});

You need to inject the TestScheduler into the timer method inside a beforeEach part:
beforeEach(function() {
this.scheduler = new TestScheduler();
this.scheduler.maxFrames = 5000; // Define the max timespan of the scheduler
const originalTimer = Observable.timer;
spyOn(Observable, 'timer').and.callFake(function(initialDelay, dueTime) {
return originalTimer.call(this, initialDelay, dueTime, this.scheduler);
});
});
After that you have full control of the time with scheduleAbsolute:
this.scheduler.schedule(() => {
// should have been called once
// You can put your test code here
}, 1999, null);
this.scheduler.schedule(() => {
// should have been called twice
// You can put your test code here
}, 2000, null);
this.scheduler.schedule(() => {
// should have been called three times
// You can put your test code here
}, 4000, null);
this.scheduler.flush();
You need scheduler.flush() to start the TestScheduler.
edit: so if you want to only test it X times, use the schedule functions as often (and with the right absolute times in milliseconds) as you wish.
edit2: I added the missing scheduler start
edit3: I changed it so should be working with RxJs5
edit4: Add maxFrames setting since the default is 750ms and will prevent testing longer-running sequences.

I had issues with the TestScheduler() approach because the schedule() arrow function would never execute, so I found another path.
The Observable.timer function just returns an Observable, so I created one from scratch to give me complete control.
First, create a var for the observer:
let timerObserver: Observer<any>;
Now in the beforeEach() create the spy and have it return an Observable. Inside the Observable, save your instance to the timer:
beforeEach(() => {
spyOn(Observable, 'timer').and.returnValue(Observable.create(
(observer => {
timerObserver = observer;
})
));
});
In the test, just trigger the Observable:
it('Some Test',()=>{
// do stuff if needed
// trigger the fake timer using the Observer reference
timerObserver.next('');
timerObserver.complete();
expect(somethingToHappenAfterTimerCompletes).toHaveBeenCalled();
});

You can test Observable timers pretty easily with fakeAsync(). Here's a component that displays a countdown timer (using a momentJS duration):
timeout.component.ts
#Component({
selector: 'app-timeout-modal',
templateUrl: './timeout-modal.component.html'
})
export class TimeoutModalComponent implements OnInit {
countdownTimer: Observable<number>;
countdownSubscription: Subscription;
durationLeft = moment.duration(60000); // millis - 60 seconds
ngOnInit() {
this.countdownTimer = Observable.timer(0, 1000);
this.countdownSubscription = this.countdownTimer
.do(() => this.durationLeft.subtract(1, 's'))
.takeWhile(seconds => this.durationLeft.asSeconds() >= 0)
.subscribe(() => {
if (this.durationLeft.asSeconds() === 0) {
this.logout();
}
});
}
}
timeout.component.spec.ts
beforeEach(async(() => {
...
}));
beforeEach(() => {
fixture = TestBed.createComponent(TimeoutModalComponent);
component = fixture.componentInstance;
});
it('should show a count down', fakeAsync(() => {
fixture.detectChanges();
expect(component.durationLeft.asSeconds()).toEqual(60);
tick(1000);
fixture.detectChanges();
expect(component.durationLeft.asSeconds()).toEqual(59);
component.countdownSubscription.unsubscribe();
}));

I was struggling with this for a while also. Since apparently a lot has changed in the frameworks since this question was asked, I thought maybe someone would be helped by my solution. My project uses rxjs 5, jasmine 2.8 and angular 5.
In my component a timer was used to call a http-get function in a service every minute. My problem was that when using fakeAsync zone the (stubbed) get function was never called and I received the error: "Error: 1 periodic timer(s) still in the queue.".
The error is showing up because the timer keeps firing and isn't stopped at the end of the test. This can be resolved by adding "discardPeriodicTasks();" to the end of the test, which causes the timer to stop. Tick(); can be used to fake to passage of time untill a next call. I used a spy on my get-function in my service to see if it worked:
it(
'should call getTickets from service every .. ms as defined in refreshTime',
fakeAsync(() => {
fixture.detectChanges();
tick();
expect(getTicketsSpy).toHaveBeenCalledTimes(1);
// let 2 * refreshtime pass
tick(2 * component.refreshTime);
expect(getTicketsSpy).toHaveBeenCalledTimes(3);
discardPeriodicTasks();
})
);
The refreshTime is the parameter that I used in the timer. I hope this prevents someone from spending half a day trying to figure this out.

Related

How to test NestJS Controller if it has a lot of dependent services?

I have a trouble with Testing Nest JS Controller because I cannot realise how to make a Testing Module with all the dependencies. I've already tried Mocks but still it's not working.
Here's how the controller I want to test looks like
calculator.controller.ts
#Controller('/calculator')
export class CalculatorController {
constructor(
#Inject(HISTORY_SERVICE)
private historyService: HistoryService,
#Inject(CACHE_SERVICE)
private readonly cacheService: CacheService,
#Inject(CALCULATOR_SERVICE)
private readonly calculatorService: CalculatorService,
) {}
#Get()
getResult(#Query() expressionDto: ExpressionDto): Promise<ClientResponseDto> {
const expression: string = expressionDto.expression;
const response: Promise<ClientResponseDto> = this.cacheService
.checkInCache(expression)
.then((response) => {
if (!response) {
const calculationResult =
this.calculatorService.getResult(expression);
const clientDto = this.historyService
.create({
expression,
result: calculationResult,
})
.then((dbResponse) => {
return this.cacheService.setToCache(dbResponse);
});
return clientDto;
}
return this.historyService.create({ expression, result: response });
});
return response;
}
}
And this is how it's spec looked like before mocks implementation
controller.spec.ts
let calculatorController: CalculatorController;
let calculatorService: CalculatorService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [HistoryModule],
controllers: [CalculatorController],
providers: [
CalculatorService,
],
})
.useMocker(() => createMock())
.compile();
calculatorController =
moduleRef.get<CalculatorController>(CalculatorController);
calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
jest.clearAllMocks();
});
describe('Calculator Controller', () => {
it('should be defined', () => {
expect(calculatorController).toBeDefined();
});
it('should have all methods', () => {
expect(calculatorController.getResult).toBeDefined();
expect(calculatorController.getResult(calculatorStub().request)).toBe(
typeof Promise,
);
});
});
And this test failed when calling getResult function cause inside this Function firstly I call CacheService to check data in Cache. So at this moment test failed telling that
TypeError: this.cacheService.checkInCache(...).then is not a function
24 | const response: Promise<ClientResponseDto> = this.cacheService
25 | .checkInCache(expression)
> 26 | .then((response) => {
| ^
I started to think that the problem is Testing module somehow doesn't have access to the Cache Service, so I added mock to the providers like this
let calculatorController: CalculatorController;
let calculatorService: CalculatorService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [HistoryModule],
controllers: [CalculatorController],
providers: [
CalculatorService,
{
provide: CacheService,
useValue: {
checkInCache: jest.fn().mockResolvedValue(Promise<null>),
},
},
],
})
.useMocker(() => createMock())
.compile();
calculatorController =
moduleRef.get<CalculatorController>(CalculatorController);
calculatorService = moduleRef.get<CalculatorService>(CalculatorService);
jest.clearAllMocks();
});
But now tests don't even run cause I have Nest dependencies problems
Nest can't resolve dependencies of the CalculatorController (HISTORY_SERVICE, ?,
CALCULATOR_SERVICE). Please make sure that the argument dependency at index [1] is
available in the RootTestModule context.
What is the issue and how is it possible to solve this problem?
Generally speaking, when unit testing a service or a controller, you want to provide mocks for the controller's or service's dependencies. Most of the time, these are going to be objects with the same method names but the methods are set to be jest.fn() or similar for other mock libraries. You'll want to use custom providers to create the mock providers that will be injected. Taking your controller above, you'll want the setup of your test to look something like this:
describe('CaclulatorController', () => {
let controller: CalculatorController;
let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
beforeAll(async () => {
const modRef = await Test.createTestModule({
controller: [CalculatorController],
providers: [
{
provide: CALCULATOR_SERVICE,
useValue: {
getResult: jest.fn(),
},
},
{
provide: CACHE_SERVICE,
useValue: {
checkInCache: jest.fn(),
setToCache: jest.fn(),
},
},
{
provide: HISTORY_SERVICE,
useValue: {
create: jest.fn(),
},
},
]
}).compile();
controller = app.get(CalculatorController);
service = app.get(CALCULATOR_SERVICE);
cache = app.get(CACHE_SERVICE);
history = app.get(HISTORY_SERVICE);
});
Okay that's a lot to look at at once, so let's step through the big parts and explain what's going on here. The first this I do is set up local variables to reference during the test for the class that I'm testing (CalculatorController) and the dependencies of the class so I can modify them as necessary. Next, I use a Pick<T, K> generic with the jest.MockedOject<T> generic to tell Typescript that "This class has been mocked by jest, and I only am worried about these methods of it" so later on when I use cache. I'll get some intellisense for the checkInCache and setToCache methods, and they'll have jest's mock function types.
In the beforeAll I set up the initial mocks for the dependencies, you can also set return values here using the appropriate mockReturnValue or mockResolvedValue methods.
Now that the mocks and dependencies are set up, we can actually write a test. My approach is to use a describe block per method and its per variation of the method's outcome and branches. I'll write a single branch to show you and let you work out the rest from there.
describe('getResult', () => {
it('should get no response from the cache and perform a full caclulation', async () => {
cache.checkInCache.mockResolvedValueOnce(undefined);
service.getResult.mockResolvedValueOnce(calculationResult);
histoy.create.mockResolvedValueOnce(dbResult);
cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
});
})
This should cover the case where there's no value in the cache and the full set of steps has to be taken. By using mockResolvedValueOnce we ensure that the methods don't return if called more than once as that's most likely not the expected case here, and we're making sure to return promsies as you use .thens. You might want to look into async/await syntax to help clean that up.
putting the two snippets together we have the following:
describe('CaclulatorController', () => {
let controller: CalculatorController;
let service: Pick<jest.MockedObject<CalculatorService>, 'getResult'>;
let cache: Pick<jest.MockedObject<CacheService>, 'checkInCache' | 'setToCache'>;
let history: Pick<jest.MockedObject<HistoryService>, 'create'>;
beforeAll(async () => {
const modRef = await Test.createTestModule({
controller: [CalculatorController],
providers: [
{
provide: CALCULATOR_SERVICE,
useValue: {
getResult: jest.fn(),
},
},
{
provide: CACHE_SERVICE,
useValue: {
checkInCache: jest.fn(),
setToCache: jest.fn(),
},
},
{
provide: HISTORY_SERVICE,
useValue: {
create: jest.fn(),
},
},
]
}).compile();
controller = app.get(CalculatorController);
service = app.get(CALCULATOR_SERVICE);
cache = app.get(CACHE_SERVICE);
history = app.get(HISTORY_SERVICE);
});
describe('getResult', () => {
it('should get no response from the cache and perform a full caclulation', async () => {
cache.checkInCache.mockResolvedValueOnce(undefined);
service.getResult.mockResolvedValueOnce(calculationResult);
histoy.create.mockResolvedValueOnce(dbResult);
cache.setInCache.mockResolvedValueOnce(cacheSaveResult);
await expect(controller.getResult({ expression: someExpression })).resolves.toEqual(cacheSaveResult)
});
});
});
That should be enough to get you started on testing the rest of your controller. If you need more test setup examples, there's an entire GitHub repository of them with different setups

Cannot test AsyncTypeahead from react-bootstrap-typeahead with Enzyme

I am trying to test AsyncTypeahead from react-bootstrap-typeahead.
I have a very simple test component :
class AsyncTypeahead2 extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
};
}
render() {
return ( <AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/> )
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id:1, stateName:"Alaska"},
{id:2, stateName:"Alabama"}
]
},
);
(Note that the URL is mocked to return two elements)
When I run this in my storybook it looks fine :
But if I want to test it (with Enzyme) it does not recognise the < li > items that pop up.
let Compoment =
<div>Basic AsyncTypeahead Example
<AsyncTypeahead2/>
</div>
const wrapper = mount(Compoment);
let json = wrapper.html();
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
Instead of finding two "dropdown-item" items there is just one item with the text "Type to Search...".
Is the AynchTypeahead not updating the DOM correctly with respect to Enzyme?
<AsyncTypeahead> is asynchronous. On the other hand simulate() is synchronous. So at the time you get to expect() AsyncTypeahead not even started to populate the dropdown with <li> elements. You need to wait for it.
It's not specified, but it looks like you are using fetch-mock package.
There is the flush function which
Returns a Promise that resolves once all fetches handled by fetch-mock have resolved
So this:
...
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
await fetchMock.flush() // !!!
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2)
should work.
...But probably it won't. Because
fetchMock.mock(...)
fetch(...)
await fetchMock.flush()
does work, but
fetchMock.mock(...)
setTimeout(() => fetch(...), 0)
await fetchMock.flush()
does not. await fetchMock.flush() returns right away if there was no call of fetch. And probably there won't be. Because <AsyncTypeahead> debounces.
(By the way, you can also try to mock fetch on a per-test basis. Just in case.)
So I see two options:
Use something else instead of fetch-mock package. Where you can resolve your own Promises on mocked requests completion.
https://tech.travelaudience.com/how-to-test-asynchronous-data-fetching-on-a-react-component-ff2ee7433d71
import waitUntil from 'async-wait-until';
...
test("test name", async () => {
let Compoment = <AsyncTypeahead2/>
...
await waitUntil(() => wrapper.state().isLoading === false);
// or even
// await waitUntil(() => wrapper.find(".dropdown-item").length === 2, timeout);
expect(...)
})
This options if not pretty. But maybe it's your only option - there is not only the fetch-mock you should worry about. setState also asynchronous... and it looks like there is no pretty way to check when it's done updating the state and the DOM without changing the real code (which is quite undesirable).
The exact solution to my problem is in the following code (copy and paste into a JS file to see it work).
Things to note :
I needed to use the waitUntil function from the async-wait-until library. fetch-mock on its own does not provide the functionality to test async code.
I needed to add an ugly hack at global.document.createRange because of some tooltip issue with react-bootstrap-typeahead and jest.
use waitUntil to wait on changes on the internal state of the component
It is very important to call wrapper.update() to update the DOM afterwards.
..
import React, {Component} from 'react';
import waitUntil from 'async-wait-until';
import {mount} from "enzyme";
import fetchMock from "fetch-mock";
import {AsyncTypeahead} from "react-bootstrap-typeahead";
describe('Autocomplete Tests ', () => {
test(' Asynch AutocompleteInput ', async () => {
class AsyncTypeaheadExample extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
finished: false
};
}
render() {
return (<AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
finished: true
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/>)
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id: 1, stateName: "Alaska"},
{id: 2, stateName: "Alabama"}
]
},
);
let Compoment =
<AsyncTypeaheadExample/>
// ugly hacky patch to fix some tooltip bug
// https://github.com/mui-org/material-ui/issues/15726
global.document.createRange = () => ({
setStart: () => {
},
setEnd: () => {
},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
let wrapper = mount(Compoment);
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', {target: {value: "al"}});
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
//now the async stuff is happening ...
await waitUntil(() => {
return wrapper.state().finished === true;
}, 3000); //wait about 3 seconds
wrapper.update() //need to update the DOM!
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
})
});
UPDATE
I can also compare on wrapper items rather than doing a direct comparison on the state :
//now the async stuff is happening ...
await waitUntil(() => {
wrapper.update() //need to update the DOM!
return wrapper.find(".dropdown-item").length > 1
}, 3000); //wait about 3 seconds
This is probably better because it means i dont need to know about the component internals.

Testing observable object angular 2 karma

I'm working on my unit test cases for Angular 2 with Karma, I got stuck with one of a function where I run the test for below line
expect(component.subscribeToEvents()).toBeTruthy();
and I view my coverage code, the lines inside the test file seems not covering anything inside the subscribe. I have tried using MockBackend in mocking the api call inside a function on service but I'm not sure how to do the mocking on a subscribed object, can somebody please help me?
The below is in test.component.ts
subscribeToEvents() {
this.subscription = this.czData.$selectedColorZone
.subscribe(items => {
this.resourceLoading = true;
if (!this.resourceData || (this.resourceData && this.resourceData.length === 0)) {
this.settings.layout.flypanel.display = false;
this.getAllResources(this.pagination.start, this.pagination.size);
}
else {
this.pagination.start = 1;
this.pagination.end = this.pagination.size;
this.getAllResources(1, this.pagination.size);
this.settings.layout.flypanel.display = true;
}
});
return true;
}
The screenshot of the coverage code
You can't do this, as the subscription is resolved asynchronously. So the synchronous test completes before the async task is resolved.
If all you want is coverage, you can just make the test async. This will cause the Angular test zone to wait until the async task is resolved, before completing the test
import { async } from '#angular/core/testing';
it('..', async(() => {
component.subscribeToEvents();
}))
You can't try to expect anything here, as there is no callback hook for when the task is resolved. So this is really a pointless test. It will give you coverage, but you aren't actually testing anything. For instance, you might want to test that the variables are set when the subscription is resolved.
Based on the code provided, what I would do instead is just mock the service, and make it synchronous. How can you do that? We you can make the mock something like
class CzDataSub {
items: any = [];
$selectedColorZone = {
subscribe: (callback: Function) => {
callback(this.items);
}
}
}
Then just configure it in the test
let czData: CzDataStub;
beforeEach(() => {
czData = new CzDataStub();
TestBed.configureTestingModule({
providers: [
{ provide: CzData, useValue: czData }
]
})
})
Now in your tests, you don't need to make it async, and you can provide any value you want by just setting the items property on the mock, and subscriber will get it
it('..', () => {
czData.items = something;
component.subscribeToEvents();
expect(component.settings.layout.flypanel.display).toBe(false);
})
UPDATE
I think I was half asleep when I wrote this post. One of the above statements is incorrect
You can't try to expect anything here, as there is no callback hook for when the task is resolved.
This is not completely true. This is what fixture.whenStable() is for. For instance if this is your service
class CzData {
_value = new Subject<>();
$selectedColorZone = this._value.asObservable();
setValue(value) {
this._value.next(value);
}
}
Then this is how you would make the test work
let czData: CzData;
let fixture: ComponentFixture<YourComponent>;
let component: YourComponent;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ CzData ],
declarations: [ YourComponent ]
});
fixture = TestBed.createComponent(YourComponent);
component = fixture.componentInstance;
czData = TestBed.get(czData);
})
it('..', async(() => {
component.subscribeToEvents();
czData.setValue(somevalue);
fixture.whenStable().then(() => {
expect(component.settings.layout.flypanel.display).toBe(false);
})
}))
We use fixture.whenStable() to to wait for the async tasks to complete.
This is not to say that using the mock is wrong. A lot of the time, using the mock would be the way to go. I just wanted to correct my statement, and show how it could be done.
Consider how Angular Outputs are tested since they are subscribed to during testing: https://angular.io/guide/testing#clicking
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selected: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroDe.triggerEventHandler('click', null);
expect(selectedHero).toBe(expectedHero);
});
So try:
const expectedItem = {}; // mock the expected result from 'subscribeToEvents'
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedItem: any; // change to expected type
component.subscribeToEvents.subscribe((item: any) => selectedItem = item);
// fixture.detectChanges(); // trigger change detection if necessary here, depending on what triggers 'subscribeToEvents'
expect(selectedItem).toBe(expectedItem);
});

Angular 2 Observable testing Error: Cannot use setInterval from within an async zone test

I'm trying to test a component, which uses a service that makes async http calls. The service returns an Observable, which the component subscribes on.
Service code snippet:
getRecentMachineTemperatures(_machine_Id): Observable<IDeviceReadings[]> {
return this.http.get(TemperatureService.URL + _machine_Id)
.map(response => { return response.json(); })
.map((records: Array<any>) => {
let result = new Array<IDeviceReadings>();
if (records) {
records.forEach((record) => {
let device = new IDeviceReadings();
device.device_id = record.device_id;
if (record.d) {
record.d.forEach((t) => {
let temperature = new ITemperature();
temperature.timestamp = t.timestamp;
temperature.value = t.temperature;
device.temperatures.push(temperature);
});
}
result.push(device);
});
}
return result;
});
}
Component code snippet:
ngOnInit() {
this.getRecentTemperatures();
}
getRecentTemperatures() {
this.temperatureService.getRecentMachineTemperatures(this.machine_id)
.subscribe(
res => {
let device1 = res[0];
this.deviceId = device1.device_id;
this.initTemperatures(device1.temperatures);
this.updateChart();
},
error => console.log(error));
}
My Test sets up dependencies, spies on the service 'getRecentMachineTemperatures' and sets i to return some stub data. I've been googling around for ways to test this, thus resulting in 3 different test, trying to test the same thing. Each giving me a different error.
temperature.component.spec.ts:
let machine_id = 1;
let comp: TemperatureComponent;
let fixture: ComponentFixture<TemperatureComponent>;
let de: DebugElement;
let el: HTMLElement;
let temperatureService: TemperatureService;
let stubDevices: IDeviceReadings[];
let stubTemperatures: ITemperature[];
let spyRecentTemps: Function;
describe('Component: Temperature', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TemperatureComponent],
imports: [ ChartsModule ],
providers: [
MockBackend,
BaseRequestOptions,
{ provide: Http,
useFactory: (backend, defaultOptions) => {
return new Http(backend, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]},
TemperatureService
]
});
stubDevices = new Array<IDeviceReadings>();
let stubDevice = new IDeviceReadings();
stubDevice.device_id = 'stub device';
stubDevice.temperatures = new Array<ITemperature>();
let stubTemp = new ITemperature();
stubTemp.timestamp = new Date().getTime();
stubTemp.value = 10;
stubDevice.temperatures.push(stubTemp);
stubDevices.push(stubDevice);
stubTemperatures = new Array<ITemperature>();
let stubTemp2 = new ITemperature();
stubTemp.timestamp = new Date().getTime() + 1;
stubTemp.value = 11;
stubTemperatures.push(stubTemp2);
fixture = TestBed.createComponent(TemperatureComponent);
comp = fixture.componentInstance;
temperatureService = fixture.debugElement.injector.get(TemperatureService);
spyRecentTemps = spyOn(temperatureService, 'getRecentMachineTemperatures')
.and.returnValue(Observable.of(stubDevices).delay(1));
// get the "temperature-component" element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.temperature-component'));
el = de.nativeElement;
});
it('should show device readings after getRecentTemperatures subscribe (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
expect(spyRecentTemps.calls.any()).toBe(true, 'getRecentTemperatures called');
tick(1000);
fixture.detectChanges();
expect(el.textContent).toContain(stubDevices[0].temperatures[0].timestamp);
expect(el.textContent).toContain(stubDevices[0].temperatures[0].value);
}));
it('should show device readings after getRecentTemperatures subscribe (async)', async(() => {
fixture.detectChanges();
expect(spyRecentTemps.calls.any()).toBe(true, 'getRecentTemperatures called');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(el.textContent).toContain(stubDevices[0].temperatures[0].timestamp);
expect(el.textContent).toContain(stubDevices[0].temperatures[0].value);
});
}));
it('should show device readings after getRecentTemperatures subscribe (async) (done)', (done) => {
async(() => {
fixture.detectChanges();
expect(spyRecentTemps.calls.any()).toBe(true, 'getRecentTemperatures called');
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(el.textContent).toContain(stubDevices[0].temperatures[0].timestamp);
expect(el.textContent).toContain(stubDevices[0].temperatures[0].value);
}).then(done);
});
});
});
fakeAsync fails with: 'Error: 1 timer(s) still in the queue.'
async fails with: 'Error: Cannot use setInterval from within an async zone test.'
async (done) fails with: 'Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'
How would I go about testing components with a async service dependency?
From what I understand it might be something about the AsyncScheduler within the Rx library using Date().now instead of faked time (https://github.com/angular/angular/issues/10127). If so has this been fixed? Or anyone found a workaround?
I'm using angular-cli: 1.0.0-beta.16. node: 4.4.2. npm: 3.10.6. webpack 2.1.0-beta.22.
I had ..
import 'rxjs/add/operator/timeout';
return this.http[method](url, emit, this.options)
.timeout(Config.http.timeout, new Error('timeout'))
Which was causing this error. I believe under the hood RXJS .timeout is calling setInterval.
I fixed this by switching ...
it('blah', async(() => {
to
it('blah', (done) => {

Testing component logic with Angular2 TestComponentBuilder

There are a lot of different approaches to unit test your angular application you can find at the moment. A lot are already outdated and basically there's no real documentation at this point. So im really not sure which approach to use.
It seems a good approach at the moment is to use TestComponentBuilder, but i have some trouble to test parts of my code especially if a function on my component uses an injected service which returns an observable.
For example a basic Login Component with a Authentication Service (which uses a BackendService for the requests).
I leave out the templates here, because i don't want to test them with UnitTests (as far as i understood, TestComponentBuilder is pretty useful for this, but i just want to use a common approach for all my unit tests, and the it seems that TestComponentBuilder is supposed to handle every testable aspect, please correct me if i'm wrong here)
So i got my LoginComponent:
export class LoginComponent {
user:User;
isLoggingIn:boolean;
errorMessage:string;
username:string;
password:string;
constructor(private _authService:AuthService, private _router:Router) {
this._authService.isLoggedIn().subscribe(isLoggedIn => {
if(isLoggedIn) {
this._router.navigateByUrl('/anotherView');
}
});
}
login():any {
this.errorMessage = null;
this.isLoggingIn = true;
this._authService.login(this.username, this.password)
.subscribe(
user => {
this.user = user;
setTimeout(() => {
this._router.navigateByUrl('/anotherView');
}, 2000);
},
errorMessage => {
this.password = '';
this.errorMessage = errorMessage;
this.isLoggingIn = false;
}
);
}
}
My AuthService:
#Injectable()
export class AuthService {
private _user:User;
private _urls:any = {
...
};
constructor( private _backendService:BackendService,
#Inject(APP_CONFIG) private _config:Config,
private _localStorage:LocalstorageService,
private _router:Router) {
this._user = _localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
}
get user():User {
return this._user || this._localStorage.get(LOCALSTORAGE_KEYS.CURRENT_USER);
}
set user(user:User) {
this._user = user;
if (user) {
this._localStorage.set(LOCALSTORAGE_KEYS.CURRENT_USER, user);
} else {
this._localStorage.remove(LOCALSTORAGE_KEYS.CURRENT_USER);
}
}
isLoggedIn (): Observable<boolean> {
return this._backendService.get(this._config.apiUrl + this._urls.isLoggedIn)
.map(response => {
return !(!response || !response.IsUserAuthenticated);
});
}
login (username:string, password:string): Observable<User> {
let body = JSON.stringify({username, password});
return this._backendService.post(this._config.apiUrl + this._urls.login, body)
.map(() => {
this.user = new User(username);
return this.user;
});
}
logout ():Observable<any> {
return this._backendService.get(this._config.apiUrl + this._urls.logout)
.map(() => {
this.user = null;
this._router.navigateByUrl('/login');
return true;
});
}
}
and finally my BackendService:
#Injectable()
export class BackendService {
_lastErrorCode:number;
private _errorCodes = {
...
};
constructor( private _http:Http, private _router:Router) {
}
post(url:string, body:any):Observable<any> {
let options = new RequestOptions();
this._lastErrorCode = 0;
return this._http.post(url, body, options)
.map((response:any) => {
...
return body.Data;
})
.catch(this._handleError);
}
...
private _handleError(error:any) {
...
let errMsg = error.message || 'Server error';
return Observable.throw(errMsg);
}
}
Now i want to test the basic logic of logging in, one time it should fail and i expect an error message (which is thrown by my BackendService in its handleError function) and in another test it should login and set my User-object
This is my current approach for my Login.component.spec:
Updated: added fakeAsync like suggested in Günters answer.
export function main() {
describe('Login', () => {
beforeEachProviders(() => [
ROUTER_FAKE_PROVIDERS
]);
it('should try and fail logging in',
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
tcb.createAsync(TestComponent)
.then((fixture: any) => {
tick();
fixture.detectChanges();
let loginInstance = fixture.debugElement.children[0].componentInstance;
expect(loginInstance.errorMessage).toBeUndefined();
loginInstance.login();
tick();
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(true);
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(false);
expect(loginInstance.errorMessage.length).toBeGreaterThan(0);
});
})));
it('should log in',
inject([TestComponentBuilder], fakeAsync((tcb: TestComponentBuilder) => {
tcb.createAsync(TestComponent)
.then((fixture: any) => {
tick();
fixture.detectChanges();
let loginInstance = fixture.debugElement.children[0].componentInstance;
loginInstance.username = 'abc';
loginInstance.password = '123';
loginInstance.login();
tick();
fixture.detectChanges();
expect(loginInstance.isLoggingIn).toBe(true);
expect(loginInstance.user).toEqual(jasmine.any(User));
});
})));
});
}
#Component({
selector: 'test-cmp',
template: `<my-login></my-login>`,
directives: [LoginComponent],
providers: [
HTTP_PROVIDERS,
provide(APP_CONFIG, {useValue: CONFIG}),
LocalstorageService,
BackendService,
AuthService,
BaseRequestOptions,
MockBackend,
provide(Http, {
useFactory: function(backend:ConnectionBackend, defaultOptions:BaseRequestOptions) {
return new Http(backend, defaultOptions);
},
deps: [MockBackend, BaseRequestOptions]
})
]
})
class TestComponent {
}
There are several issues with this test.
ERROR: 'Unhandled Promise rejection:', 'Cannot read property 'length' of null' I get this for the test of `loginInstance.errorMessage.length
Expected true to be false. in the first test after i called login
Expected undefined to equal <jasmine.any(User)>. in the second test after it should have logged in.
Any hints how to solve this? Am i using a wrong approach here?
Any help would be really appreciated (and im sorry for the wall of text / code ;) )
As you can't know when this._authService.login(this.username, this.password).subscribe( ... ) is actually called you can't just continue the test synchronically and assume the subscribe callback has happened. In fact it can't yet have happened because sync code (your test) is executed to the end first.
You can add artificial delays (ugly and flaky)
You can provide observables or promises in your component that emit/resolve when something you want to test is actually done (ugly because test code added to production code)
I guess the best option is using fakeAsync which provides more control about async execution during tests (I haven't used it myself)
As far as I know there will come support in Angular tests using zone, to wait for the async queue to become empty before the test continues (I don't know details about this neither).