Beginner’s Guide to Ionic Angular Unit Testing (Part 3) — Async Testing
In this series, we are talking about Angular Testing using an Ionic Angular application. Till now we have seen the basic setup process, the understanding of the core concept of Unit Testing — in mainly Components Testing. However, we have not touched upon a very important aspect of Javascript programming — ASYNC functions(which have been making promises friendly). Async functions always return a promise.
Yes, all functions which have return values as Promise OR Observable — which makes our lives hard as a programmer 😅
For those who have been not following this series — till now, we have created a simple Ionic Angular application with the following:
- List component for displaying Jobs (A Simple TODO Style App).
- Service called
ApiService
-This List component adds data and gets data from a Service calledApiService
Now for this part of series, we will call an AJAX call of Github Jobs API to fetch few Real life jobs posted over the web.
Preparations 🧑🍳
fetchJobs(){
return this .http .get('https://github-jobs-proxy.appspot.com/positions?description=javascript&location=san+francisco');
}
We have this simple API call to our api.service.ts
file which we are using to fetch Jobs. Don’t forget to import HTTPClient
and then do the required initialization in the constructor.
We have used a proxies URL to bypass the CORS issue which happens when you call the official GitHub API for Jobs.
In our Tab1 Component file, we are adding a call to this function.
ngOnInit(){
this.jobs = this.apiService.getJobs(); this.githubJobs = this.apiService.fetchJobs(); // << ADD THIS }
Now as a final part of our preparation let’s add this to our HTML template. As we know this http.get() will return an Observable. By using the async pipe in Angular, in our template, we can automatically subscribe to it.
<ion-item>
<ion-label color="secondary">Github Jobs By Observable</ion-label>
</ion-item>
<ion-list>
<ion-item *ngFor="let job of githubJobs | async">
<ion-label>{{job.title}}</ion-label>
</ion-item>
</ion-list>
As you can see we have added a list in which we are using *ngFor
to loop over each item provided by Observable.
Testing
From the previous part of this series, we know that — we are using a Mock/Stub Service for APIServicein out Test File — we will need to Stub the new Function as well — I will add a line for this in our Spec File.
const ApiServiceStub = {
addJob: () => null,
fetchJobs: () => from([[{title: 'job1'}, {title: 'job2'}]]),
getJobs: () => []
};
You can observe that there are 2 brackets [[ in above. We are using from
rxjs operator which takes an Array as input & it returns each element one by one.
We only need to return 1 value. We know that Jobs
array is expected returned value for this function. So we create a single data — an array inside an array. This will mimic the Github API response — which also returns only 1 array.
We have just used Title in our application, so we have simplified the object also in testing.
If you are new to Testing and wondering why in God’s name this API function call is being stubbed — while we have original API available to test with. Please understand this is Unit Testing and component doesn’t want to be dependent on the Service or anything.
We just have the assumption that service will return this kind of output — and that is what component expects. If this expectation is not right, then we need to work on Service rather than changing our component’s logic.
Now let’s add a test to our tab1.page.spec.ts
it('fetchJobs should return Observable which contains array of Jobs', () => {
fixture.detectChanges();
component.githubJobs.subscribe((result) => { expect(result.length).toBeDefined(); expect(result[0].title).toBeDefined(); });
});
You can see that we have subscribed to the Observable to get its value. Before that, we have called fixture.detectChanges()
which will call the ngOnInit
function. As it will immediately emit the array we have added to the stub. This test will work fine.
Let run ng test
All passing!
Using Promises
We just went through an easy part of testing with Observable. Let’s move to a more complex part of working with Promises.
We will adjust our Github API function to return a Promise rather than observable. In fact, we will make a copy of the function just to have both an Observable and Promise style working together.
fetchJobsByPromise(){
return this.http .get('https://github-jobs-proxy.appspot.com/positions?description=javascript&location=san+francisco') .toPromise();
}
We added fecthJobsByPromise
function to our service file. It uses toPromise
function to convert the Observable output to Promise based Output.
Next in Component/Page, we will add a call to this service function.
ngOnInit(){
this.jobs = this.apiService.getJobs(); this.githubJobs = this.apiService.fetchJobs();
this.apiService.fetchJobsByPromise().then((jobs) => { this.githubJobsByPromise = jobs; });
}
As you can see above, we have created githubJobsByPromise
— a view variable to store the result of the resolved promise. Now we will change the template to have a very similar list of jobs from this githubJobsByPromise
.
<ion-item>
<ion-label color="secondary">Github Jobs By Promise</ion-label>
</ion-item>
<ion-list>
<ion-item *ngFor="let job of githubJobsByPromise">
<ion-label>{{job.title}}</ion-label>
</ion-item>
</ion-list>
All set. Let’s run the whole thing.
Back to testing.
We are adding a very small test which will check the value of githubJobsFromPromise
it('fetchJobByPromise should be array of Jobs', () => {
fixture.detectChanges();
expect(component.githubJobsByPromise.length).toBeDefined(); expect(component.githubJobsByPromise[0].title).toBeDefined();
});
If your test server is still running you can check the browser window of Karma Server.
So, we have missed adding the Promise implementation on our Service Stub — So you can face a similar error.
const ApiServiceStub = {
addJob: () => null, fetchJobs: () => from([[{title: 'job1'}, {title: 'job2'}, {title: 'job3'}]]),
fetchJobsByPromise: () => Promise.resolve([{title: 'job1'}, {title: 'job2'}, {title: 'job3'}]),
getJobs: () => []
};
Added it — a simple Promise which will resolve in a job object array.
Let's check again.
So, what happened here. This is due to the asynchronous nature of promise and the way Javascript executes. expect
calls are executed even before the promise has resolved.
You can put some kind of setTimeout
hack for expect and maybe it works sometimes but that’s not a proper way.
Async
Don’t get it wrong !!
This async
is not the same as in async/await. This a special function we can use in Angular jasmine testing.
We will import it at top :
import { async, ComponentFixture, TestBed} from '@angular/core/testing';
Async wraps the test functions — which we think will have promises and we want to wait till they are resolved
it('fetchJobByPromise should be array of Jobs', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { expect(component.githubJobsByPromise.length).toBeDefined(); expect(component.githubJobsByPromise[0].title).toBeDefined(); });
}));
Also, you can see a special function fixture.whenStable()
which is called to wrap the expected part. This part will run once all the Promises will be resolved from the Angular component.
Let’s check again
FakeAsync and Tick
There is one more way is to do asynchronous testing — using FakeAsync and Tick functions. Let us move to testing of asynchronous code with FakeAsync in Angular.
Import these here :
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
Now you can wrap the function in fakeAsync and then call the tick() just before the lines — it will act as a way to stop executing till JS resolves the Promises.
it('fetchJobByPromise should be array of Jobs (Using Fake Async)', fakeAsync(() => {
fixture.detectChanges();
tick(); expect(component.githubJobsByPromise.length).toBeDefined(); expect(component.githubJobsByPromise[0].title).toBeDefined();
}));
Let’s check this test result
The main difference between fakeAysnc and async, in angular testing, is the style of calling — while whenStable().then()
calls can create an ugly nested structure when we have many async calls to test inside one function. However, there are some actual differences also — like for example XHR calls are mostly tested with async
calls only.
Let’s try something new here, I will add some timeout in our Stub function to have a delay (4000ms) in Promise resolution.
fetchJobsByPromise: () => {
return new Promise((resolve, reject) => { setTimeout(() => { resolve([{title: 'job1'}, {title: 'job2'}, {title: 'job3'}]); }, 4000); });
}
Now if we run tests again.
So async
has survived this timeout but fakeAsync
didn’t. How can it be done with tick now — use tick(4000)
. Yes, tick
has a method pass delay also. Now it will work like charm.
Done CallBack
There is one more style of Jasmine testing which is — in-built in Jasmine wheredone
an argument is passed as 1st argument in test function. It acts as a callback — which holds the test until the time done
is not executed.
For the test where there is an unexpected delay in the execution, we can use this as a final resort.
it('fetchJobByPromise should be array of Jobs (done callback)', (done) => {
fixture.detectChanges();
setTimeout(() => {
expect(component.githubJobsByPromise.length).toBeDefined(); expect(component.githubJobsByPromise[0].title).toBeDefined(); done(); }, 5000); // delay is more than time for Promise resolve
});
In the above example, you can check how done( ) callback is used. This example is not the best one as we have created some delay deliberately to test. But we can understand how it holds the test for that duration — which otherwise will fail or say that no expectation was found.
Let’s check the test results
You might not have seen this error before, we have a test timeout limit reached. As tests can’t run forever there is a Timeout decided by Karma. Here the default is 5000ms which exactly matching our delay. Hence it failed.
So, we can decrease the timeout, but…….
…… If in future we need to run a longer test case what you will do? That’s why we will not change our code but will increase the timeout threshold.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 100000;
Add this line to beforeEach
section — this will change the timeout. Let’s check out now.
Finally ✌️
Here is our final Test file looks like