<TLDR>
Testing an Azure Function is no different than testing any Javascript module exporting an async function. Passing a mocked Azure context is tricky so use an npm module for that. Mock parts of your code making network calls.
Function app full code before tests
Function app full code with tests
Diff with only Jest config and tests
Below is a step by step guide on how to add Jest tests to an existing Azure function.</TLDR>
When I first started using Azure a few months ago I was surprised with how little information there was online, compared to the abundance of resources for AWS.
With AWS whenever you have a question chances are high someone on the Internet already had a similar problem. With Azure I found myself resolving roadblocks by trial and error again and again.
This post should provide you with enough information to start unit testing your HTTP-triggered TypeScript functions with Jest.
Function under test
We won't go into the details of creating and running a function app locally, if you need some help with that I suggest checking the official quickstart.
To start, we have a HTTP-triggered function called testable-http-triggered-function
which accepts GET requests and expects a single parameter ip
.
It will fetch the information about that IP from the IpInfo public API and return a JSON with a single field - the city of the IP:
$ curl -XGET 'http://localhost:7071/api/testable-http-triggered-function?ip=161.185.160.93'
{"city":"New York City"}
We don't want to bring side effects of network calls into our tests so we will mock the API call.
Let's check the function code quickly.
An entry point for the Function App runtime. It verifies the IP query parameter is set, makes the IpInfo API call, and returns the city as a response.
// testable-http-triggered-function/index.ts
import { AzureFunction, Context } from '@azure/functions'
import { getIpInfo } from './ipinfo'
import { responseFactory, FunctionResponse } from './util/responseFactory'
const httpTrigger: AzureFunction = async function (
context: Context,
): Promise<FunctionResponse> {
const ip = context.req.query.ip
if (!ip) {
return responseFactory({ code: 'inputValidationFailed' }, 400)
}
const ipInfo = await getIpInfo(ip)
return responseFactory({ city: ipInfo.city })
}
export default httpTrigger
Wrapper for the IpInfo fetching code. It also defines the interface of what the external API response looks like.
// testable-http-triggered-function/ipinfo.ts
import fetch from 'node-fetch'
interface IpInfoResponse {
ip: string
city: string
region: string
country: string
loc: string
postal: string
timezone: string
readme: string
}
export async function getIpInfo(ip: string): Promise<IpInfoResponse> {
const url = `https://ipinfo.io/${ip}/geo`
const res = await fetch(url)
const json = res.json()
return json
}
Utility function to standardize the function response format.
// testable-http-triggered-function/utils/responseFactory.ts
export interface FunctionResponse {
statusCode: number
body: string
headers: Record<string, string>
}
export function responseFactory(body: any, httpCode = 200): FunctionResponse {
return {
statusCode: httpCode,
body: JSON.stringify(body),
headers: {
'content-type': 'application/json; charset=utf-8',
},
}
}
Tests
We will install and configure Jest first.
Then we will add tests for the successful and error scenarios.
Install and configure Jest
We install Jest itself, its typings, and ts-jest
to be able to execute tests in TypeScript, without compiling to Javascript first.
npm i --save-dev jest @types/jest ts-jest
Azure function handler expects context
object passed as the first parameter. It encapsulates request and response objects as well as information about function bindings. Normally it's prepared by the Azure runtime but in tests, we need to craft it ourselves. There are a lot of nested objects and duplicated bits of data in the context
object so assembling it manually can be tedious. Luckily there's a carefully made stub-azure-function-context module which helps with stubbing the context.
npm i --save-dev stub-azure-function-context
Next, create jest.config.js
in the root folder. It will tell Jest to use ts-jest
to compile TypeScript test files.
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
}
Finally, add a new script to run Jest:
// package.json
"scripts": {
...
"test": "jest --verbose"
}
Add tests
1. Test for the input validation error scenario
Let's add our first test to verify our function responds with a correct error code when the ip
query parameter is missing.
The test itself should be trivial, mockedRequestFactory
deserves a comment though. It may look scary but what it does is it configures the bindings in the same way the function expects them to be. If you check the function configuration at testable-http-triggered-function/function.json
you will see it mostly matches the mocked request. A notable addition is the createHttpTrigger
call - it's what defines the mocked request: hostname, path, parameters, headers, etc.
Here, we only care about the ip
query parameter since it will be different among tests, thus we make it configurable.
// testable-http-triggered-function/__tests__/index.test.ts
import httpTrigger from '../index'
import {
runStubFunctionFromBindings,
createHttpTrigger,
} from 'stub-azure-function-context'
describe('azure function handler', () => {
it('fails on missing ip parameter', async () => {
const res = await mockedRequestFactory('')
expect(res.statusCode).toEqual(400)
const body = JSON.parse(res.body)
expect(body.code).toEqual('inputValidationFailed')
})
})
async function mockedRequestFactory(ip: string) {
return runStubFunctionFromBindings(
httpTrigger,
[
{
type: 'httpTrigger',
name: 'req',
direction: 'in',
data: createHttpTrigger(
'GET',
'http://example.com',
{},
{},
undefined,
{ ip },
),
},
{ type: 'http', name: '$return', direction: 'out' },
],
new Date(),
)
}
Let's make sure it passes. Fire npm run test
:
> jest --verbose
PASS testable-http-triggered-function/__tests__/index.test.ts
azure function handler
✓ fails on missing ip parameter (7 ms)
2. Test for the success scenario
Let's add a test where we pass the ip
query parameter which triggers the branch of code where the external API is called.
We need to mock the actual network request, that is when fetch()
is being called in our code. For that, we mock the node-fetch
module.
// testable-http-triggered-function/__tests__/index.test.ts
...
import fetch from 'node-fetch'
import { Response } from 'node-fetch'
jest.mock('node-fetch')
Let's add the test. We tell the mocked fetch
to resolve with a given city response and call our function under test.
We verify the response HTTP code and body as well as make sure our mock was called once with the IP we provided.
// testable-http-triggered-function/__tests__/index.test.ts
...
it('returns city', async () => {
const ip = '127.0.0.1'
const city = 'Los Angeles'
const mock = (fetch as unknown) as jest.Mock
mock.mockResolvedValue(new Response(JSON.stringify({ city })))
const res = await mockedRequestFactory(ip)
expect(res.statusCode).toEqual(200)
const body = JSON.parse(res.body)
expect(body.city).toEqual(city)
expect(mock).toHaveBeenCalledTimes(1)
expect(mock).toHaveBeenCalledWith(`https://ipinfo.io/${ip}/geo`)
})
Let's run the tests again...
> jest --verbose
FAIL testable-http-triggered-function/__tests__/index.test.ts
azure function handler
✓ fails on missing ip parameter (9 ms)
✕ returns city (4 ms)
● azure function handler › returns city
TypeError: res.json is not a function
16 |
17 | const res = await fetch(url)
> 18 | const json = res.json()
How come res.json
is not a function? We've mocked the entire node-fetch
module, Response
class included and the method is no longer there. What we want in this case is the Response
object to be the real implementation, not a mock.
// testable-http-triggered-function/__tests__/index.test.ts
// remove this
// import { Response } from 'node-fetch'
// and add this
const { Response } = jest.requireActual('node-fetch')
Running our tests again:
> jest --verbose
PASS testable-http-triggered-function/__tests__/index.test.ts
azure function handler
✓ fails on missing ip parameter (6 ms)
✓ returns city (14 ms)
Great! We've covered a basic Azure function with unit tests. Hopefully, that gives you an idea of how to implement Jest tests for your functions.
If you like this type of content you can follow me on Twitter for the latest updates.
Top comments (0)