Tuesday 26 November 2019

javascript - How can I test a class which contains imported async methods in it?



This is my first time working with tests and I get the trick to test UI components. Now I am attempting to test a class which has some static methods in it. It contains parameters too.



See the class:




import UserInfoModel from '../models/UserInfo.model';
import ApiClient from './apiClient';
import ApiNormalizer from './apiNormalizer';
import Article from '../models/Article.model';
import Notification from '../models/Notification.model';
import Content from '../models/Link.model';

export interface ResponseData {
[key: string]: any;

}

export default class ApiService {
static makeApiCall(
url: string,
normalizeCallback: (d: ResponseData) => ResponseData | null,
callback: (d: any) => any
) {
return ApiClient.get(url)
.then(res => {

callback(normalizeCallback(res.data));
})
.catch(error => {
console.error(error);
});
}


static getProfile(callback: (a: UserInfoModel) => void) {
return ApiService.makeApiCall(`profile`, ApiNormalizer.normalizeProfile, callback);

}
}


I already created a small test which is passing but I am not really sure about what I am doing.



// @ts-ignore
import moxios from 'moxios';
import axios from 'axios';
import { baseURL } from './apiClient';

import { dummyUserInfo } from './../models/UserInfo.model';

describe('apiService', () => {
let axiosInstance: any;

beforeEach(() => {
axiosInstance = axios.create();
moxios.install();
});


afterEach(() => {
moxios.uninstall();
});

it('should perform get profile call', done => {
moxios.stubRequest(`${baseURL.DEV}profile`, {
status: 200,
response: {
_user: dummyUserInfo
}

});

axiosInstance
.get(`${baseURL.DEV}profile`)
.then((res: any) => {
expect(res.status).toEqual(200);
expect(res.data._user).toEqual(dummyUserInfo);
})
.finally(done);
});

});


I am using moxios to test the axios stuff -> https://github.com/axios/moxios



So which could be the proper way to test this class with its methods?


Answer



Introduction



Unit tests are automated tests written and run by software developers to ensure that a section of an application meets its design and behaves as intended. As if we are talking about object-oriented programming, a unit is often an entire interface, such as a class, but could be an individual method.




The goal of unit testing is to isolate each part of the program and show that the individual parts are correct. So if we consider your ApiService.makeApiCall function:



  static makeApiCall(
url: string,
normalizeCallback: (d: ResponseData) => ResponseData | null,
callback: (d: any) => any
) {
return ApiClient.get(url)
.then((res: any) => {

callback(normalizeCallback(res.data));
})
.catch(error => {
console.error(error);
});
}


we can see that it has one external resource calling ApiClient.get which should be mocked. It's not entirely correct to mock the HTTP requests in this case because ApiService doesn't utilize them directly and in this case your unit becomes a bit more broad than it expected to be.




Mocking



Jest framework provides great mechanism of mocking and example of Omair Nabiel is correct. However, I prefer to not only stub a function with a predefined data but additionally to check that stubbed function was called an expected number of times (so use a real nature of mocks). So the full mock example would look as follows:



/**
* Importing `ApiClient` directly in order to reference it later
*/
import ApiClient from './apiClient';

/**

* Mocking `ApiClient` with some fake data provider
*/
const mockData = {};

jest.mock('./apiClient', function () {

return {
get: jest.fn((url: string) => {
return Promise.resolve({data: mockData});
})

}
});


This allows to add additional assertions to your test example:



it('should call api client method', () => {
ApiService.makeApiCall('test url', (data) => data, (res) => res);
/**
* Checking `ApiClient.get` to be called desired number of times

* with correct arguments
*/
expect(ApiClient.get).toBeCalledTimes(1);
expect(ApiClient.get).toBeCalledWith('test url');
});


Positive testing



So, as long as we figured out what and how to mock data let's find out what we should test. Good tests should cover two situations: Positive Testing - testing the system by giving the valid data and Negative Testing - testing the system by giving the Invalid data. In my humble opinion the third branch should be added - Boundary Testing - Test which focus on the boundary or limit conditions of the software being tested. Please, refer to this Glossary if you are interested in other types of tests.




The positive test flow flow for makeApiCall method should call normalizeCallback and callback methods consequently and we can write this test as follows (however, there is more than one way to skin a cat):



  it('should call callbacks consequently', (done) => {
const firstCallback = jest.fn((data: any) => {
return data;
});
const secondCallback = jest.fn((data: any) => {
return data;
});

ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {
expect(firstCallback).toBeCalledTimes(1);
expect(firstCallback).toBeCalledWith(mockData);

expect(secondCallback).toBeCalledTimes(1);
expect(secondCallback).toBeCalledWith(firstCallback(mockData));
done();
});
});



Please, pay attention to several things in this test:
- I'm using done callback to let jest know the test was finished because of asynchronous nature of this test
- I'm using mockData variable which the data that ApiClient.get is mocked this so I check that callback got correct value
- mockData and similar variables should start from mock. Otherwise Jest will not allow to out it out of mock scope



Negative testing



The negative way for test looks pretty similar. ApiClient.get method should throw and error and ApiService should handle it and put into a console. Additionaly I'm checking that none of callbacks was called.




import ApiService from './api.service';

const mockError = {message: 'Smth Bad Happened'};

jest.mock('./apiClient', function () {

return {
get: jest.fn().mockImplementation((url: string) => {
console.log('error result');

return Promise.reject(mockError);
})
}
});

describe( 't1', () => {
it('should handle error', (done) => {
console.error = jest.fn();

const firstCallback = jest.fn((data: any) => {

return data;
});
const secondCallback = jest.fn((data: any) => {
return data;
});
ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {
expect(firstCallback).toBeCalledTimes(0);
expect(secondCallback).toBeCalledTimes(0);
expect(console.error).toBeCalledTimes(1);

expect(console.error).toBeCalledWith(mockError);
done();
});
});
});


Boundary testing



Boundary testing could be arguing in your case but as long as (according to your types definition normalizeCallback: (d: ResponseData) => ResponseData | null) first callback can return null it could be a good practice to check if is the successfully transferred to a second callback without any errors or exceptions. We can just rewrite our second test a bit:




it('should call callbacks consequently', (done) => {
const firstCallback = jest.fn((data: any) => {
return null;
});
const secondCallback = jest.fn((data: any) => {
return data;
});
ApiService.makeApiCall('test url', firstCallback, secondCallback)
.then(() => {

expect(firstCallback).toBeCalledTimes(1);
expect(firstCallback).toBeCalledWith(mockData);
expect(secondCallback).toBeCalledTimes(1);
done();
});
});


Testing asynchronous code




Regarding testing asynchronous code you can read a comprehensive documentation here. The main idea is when you have code that runs asynchronously, Jest needs to know when the code it is testing has completed, before it can move on to another test. Jest provides three ways how you can do this:




  1. By means of a callback



    it('the data is peanut butter', done => {
    function callback(data) {
    expect(data).toBe('peanut butter');
    done();
    }


    fetchData(callback);
    });


    Jest will wait until the done callback is called before finishing the test. If done() is never called, the test will fail, which is what you want to happen.


  2. By means of promises



    If your code uses promises, there is a simpler way to handle asynchronous tests. Just return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will automatically fail.


  3. async/await syntax




    You can use async and await in your tests. To write an async test, just use the async keyword in front of the function passed to test.



    it('the data is peanut butter', async () => {
    const data = await fetchData();
    expect(data).toBe('peanut butter');
    });




Example



Here you can find a ready to use example of your code
https://github.com/SergeyMell/jest-experiments
Please, let me know if something left unclear for you.



UPDATE (29.08.2019)



Regarding your question





Hi, what can I do to mock ./apiClient for success and error in the same file?




According to the documentation Jest will automatically hoist jest.mock calls to the top of the module (before any imports). It seems that you can do setMock or doMock instead, however, there are issues with mocking this way that developers face from time to time. They can be overridden by using require instead of import and other hacks (see this article) however I don't like this way.



The correct way for me in this case is do split mock defining and implementation, so you state that this module will be mocked like this



jest.mock('./apiClient', function () {
return {

get: jest.fn()
}
});


But the implementation of the mocking function differs depending on scope of tests:



describe('api service success flow', () => {

beforeAll(() => {

//@ts-ignore
ApiClient.get.mockImplementation((url: string) => {
return Promise.resolve({data: mockData});
})
});

...
});

describe('api service error flow', () => {


beforeAll(() => {
//@ts-ignore
ApiClient.get.mockImplementation((url: string) => {
console.log('error result');
return Promise.reject(mockError);
})
});

...

});


This will allow you to store all the api service related flows in a single file which is what you expected as far as I understand.
I've updated my github example with api.spec.ts which implements all mentioned above. Please, take a look.


No comments:

Post a Comment

php - file_get_contents shows unexpected output while reading a file

I want to output an inline jpg image as a base64 encoded string, however when I do this : $contents = file_get_contents($filename); print &q...