Mocking API calls
I spoke about API automation last and provided a few things to look into when preparing for API automation. One of the most challenging aspects of API automation though is mocking out API responses. Something which is very important if you want your automation and testing to cover a wide range of coverage without being too dependent on an underlying system or API. Mocking an API call gives you control in these situations and speeds up development down the line.
The biggest challenge of mocking APIs itself is actually having the needed information on the underlying systems so that you can sufficiently mock it. In a mock, you want to mimic the behaviour of that system and how it responds to your APIs own behaviour and to mock this effectively you will need details of exactly how that interaction should look. This may not always be possible when you start gathering the information to mock, but the side effect of doing this properly is that it forces your teams to better document and understanding the different system interaction and in the process of mocking your APIs, you will learn a great deal about its interaction, its dependencies and hopefully have their behaviour all documented correctly in the mock itself, so the benefit of doing this far outweighs just its ability to support your automation efforts.
Below I will take you through some steps to mock an API. There are many different ways of doing this, but for the sake of my article, I will use Jasmine and Jest as frameworks to be able to work a REST API written in JavaScript. It’s what I am perhaps most familiar with, but also approaches that I know are quite widely used in the industry.
To start you will need to know the raw data of what each request and response message looks like. For example:
Test: Returns a 200 status with a user’s posts when requested
Request: {method:‘get’,body:{user:“John Smith”}}
Response: {status:200,posts:posts:[“My API works!”]}
In order to test these behaviours without going to a network, you should have this logic separate from your main routes. This way you can easily import the same functions your API depends on in your tests. You can create a function that processes a request object and returns a promise to simulate an asynchronous API call. It could look something like this:
function simulateAsyncCall(request){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
switch(request.method){
case 'get':
const user=getUser(request)
if(user){
resolve({status:200,posts:user.posts})
} else {
resolve({status:404, message:"Not Found"})
}
break;
case 'post':
if(passwordIsValid(request)){
addToPosts(request)
resolve({status:200,message:"Added Post"})
} else{
resolve({status:401,message:"Unauthorized"})
}
break;
default :
resolve({status:400, message:'Bad Request'})
}
},300)
})
}
Create a Mock API with Jasmine
Before installing any Node modules, you should make sure you have a package.json file in your root directory. If not, you can set it up with this command: npm init -y.
Let’s start by installing Jasmine in your project directory:
npm install jasmine –save-dev
When that is finished you can configure Jasmine using this command:
node node_modules/jasmine/bin/jasmine.js init
This will create a spec-folder with a configuration file with some default settings. These test files need to reside in the same folder and end with a discernible suffix. In this example, “spec.js”
We will need to set up the test command in our package.json file. Open this file and add a test key to the scripts object with the path to the jasmine module. It should look like this:
"scripts":{
"test":"jasmine"
}
To make it works you, you can test it by creating a file (“npm run test”) in the spec folder and copy the following code into it. If your test passes, then you know it is working correctly:
describe("My Jasmine Setup", function(){
var a=true;
it("tests if the value of a is true", function(){
expect(a).toBe(true)
})
})
Another common feature of test suites which I described in my last article are the “beforeEach” and “afterEach” function. This allows you to do something either before or after each test block. Let’s create a new instance of our MockAPI class before each test.
The following script should work in either Jasmine or Jest:
const MockAPI= require('../MockAPI.js');
describe("Mock API",()=>{
let mockAPI;
let mockDatabase=
{
users:[
{
name:"Vader",
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123",
posts:[" Obi-Wan never told you what happened to your father"]
},
{
name:"Luke",
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"
posts:[" He told me enough!"]
},
]
};
beforeEach(()=>{
mockAPI= new MockAPI(mockDatabase)
})
it("returns a 400 bad request status if the request is invalid",()=>{
const mockApiCall=mockAPI.simulateAsyncCall({})
return mockApiCall.then(response=>{
expect(response.status).toBe(400)
})
})
describe("get requests",()=>{
const validRequest={method:'get',body:{user:"Vader"}};
const invalidRequest={method:'get',body:{user:"Palpatine"}};
it("returns a 404 status if a user is not found",()=>{
const mockApiCall=mockAPI.simulateAsyncCall(invalidRequest)
return mockApiCall.then(response=>{
expect(response.status).toBe(404)
})
});
it("returns a 200 status with a user's posts",()=>{
const mockApiCall=mockAPI.simulateAsyncCall(validRequest)
return mockApiCall.then(response=>{
expect(response.status).toBe(200)
expect(response.posts).toEqual(["Obi-Wan never told you what happened to your father"])
})
});
})
describe("post requests",()=>{
const validRequest={method:'post',body:{user:"Vader",password:'lightside',post:"No I am your father!"}}
const invalidRequest={method:'post',body:{user:"Vader",password:'darkside',post:"He was your father. And I killed him... Hahaha"}}
it("returns a 401 unauthorized status if the wrong credentials are sent",()=>{
const mockApiCall=mockAPI.simulateAsyncCall(invalidRequest)
return mockApiCall.then(response=>{
expect(response.status).toBe(401)
expect(mockAPI.db).toEqual(mockDatabase)
})
})
it("returns a 200 status and adds the post to the database",()=>{
const newDatabase={
users:[
{
name:"Vader",
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"
posts:["Obi-Wan never told you what happened to your father."]
},
{
name:"Luke",
passwordHash:"dasdKDKDJSLASDLASDJSAasdsdc123"
posts:["He told me enough!"]
},
]
}
const mockApiCall=mockAPI.simulateAsyncCall(validRequest)
return mockApiCall.then(response=>{
expect(response.status).toBe(200)
expect(mockAPI.db).toEqual(newDatabase)
})
})
})
})
Mock API Calls With Jest
To get started with Jest, you only need to install it:
npm install jest –save-dev
And include a test command in your package.json file like this:
"scripts":{
"test":" jest"
}
Jest started as a fork of Jasmine before Facebook built on it further, so you can do everything we described above and more. This basic pattern of a “describe” block, and an “it” block which contains one or more “expect” methods works the same in Jest.
So far, we have been testing deterministic functions (which always have the same output for a given input). However, often in our API testing, we work with APIs outside our control, from other third-party applications.
With Jest we can create mock functions which return predictable outcomes and include extra methods to track how the function is integrating with the API. Using the jest.fn method, we can make assertions like this:
describe("AJAX functions with Jest",()=>{
const mockUrl='/api/users';
const mockUsers=[{name:'Vader',name:'Luke'}];
const getUsers = jest.fn((url)=>mockUsers)
test("returns returns users from an api call",()=>{
expect(getUsers(mockUrl)).toBe(mockUsers)
console.log(getUsers)
})
test("called getUser with a mockUrl",()=>{
expect(getUsers).toHaveBeenCalledWith(mockUrl)
})
})
You can also mock out entire modules (replacing their methods with jest mock functions) using jest.mock(). For example, you could import an HTTP library and set the return value of its .get() method like this:
const axios=require('axios');
jest.mock('axios');
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
const mockUsers=[{name:'Vader'}]
const mockResponse={data:mockUsers}
axios.get.mockResolvedValue(mockResponse)
return Users.all().then(data => expect(data).toEqual(users));
Jasmine also has a plugin (jasmine-ajax) used to mock AJAX calls, but it isn’t as flexible as Jest. It replaces the XMLHttpRequest object in a browser with a custom response. Since the XMLHttpRequest exists in the DOM, you would need to create a fake DOM (using something like jsdom) to run this on the backend.
These two examples essentially show how easy it can be to mock APIs and hopefully by setting up mocks in your own repos for the different tests you require, it will allow you to do some extensive API automation and get the coverage you need to develop a high-quality API.