Writing tests for HTTP-triggered Azure Functions

June 19th 2026 Unit Testing TypeScript Azure

As I was implementing redirects in Azure Functions for my blog, I wanted to be able to test them as efficiently as possible. Although you can run and test an Azure function locally, and I did, I also implemented automated unit tests to systematically cover all the cases I care about.

I chose Jest as my testing framework or ts-jest to be precise since I used Typescript. I set it up in my api folder by simply following the instructions:

npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init

I modified my package.json to invoke jest when running npm test:

{
  "scripts": {
    "test": "jest"
  }
}

And I added jest types to my tsconfig.json so that I didn't have to import test, expect and other Jest symbols:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

Since an HTTP-triggered Azure Function accepts HttpRequest and InvocationContext as parameters, I had to mock these two first to make my tests simpler.

For InvocationContext, I created a minimal mock I could use with every call. Because I prefer writing TypeScript in strict mode, I had to set all non-optional fields even if I don't need them in my code:

class MockContext implements InvocationContext {
  invocationId: string = "";
  functionName: string = "";
  extraInputs: InvocationContextExtraInputs = {
    get: function (): unknown {
      throw new Error("Function not implemented.");
    },
    set: function (inputOrName: FunctionInput | string, value: unknown): void {
      throw new Error("Function not implemented.");
    },
  };
  extraOutputs: InvocationContextExtraOutputs = {
    set: function (output: HttpOutput): void {
      throw new Error("Function not implemented.");
    },
    get: function (outputOrName: FunctionOutput | string): unknown {
      throw new Error("Function not implemented.");
    },
  };
  log(...args: any[]): void {
    console.log("[test] log:", ...args);
  }
  trace(...args: any[]): void {
    console.trace("[test] trace:", ...args);
  }
  debug(...args: any[]): void {
    console.debug("[test] debug:", ...args);
  }
  info(...args: any[]): void {
    console.info("[test] info:", ...args);
  }
  warn(...args: any[]): void {
    console.warn("[test] warn:", ...args);
  }
  error(...args: any[]): void {
    console.error("[test] error:", ...args);
  }
  retryContext?: RetryContext;
  traceContext?: TraceContext;
  triggerMetadata?: TriggerMetadata;
  options: EffectiveFunctionOptions = {
    trigger: {
      type: "",
      name: "",
    },
    extraInputs: [],
    extraOutputs: [],
  };
}

The HttpRequest contains the custom x-ms-original-url header which serves as the only relevant input to my function, so I simply create the mocked HttpRequest instance inside the helper function I use to invoke the function under test:

const baseUrl = "https://www.myserver.com";

async function invokeRedirect(route: string): Promise<HttpResponseInit> {
  const context = new MockContext();
  const request = new HttpRequest({
    url: "http://localhost/api/redirect",
    method: "GET",
    headers: { "x-ms-original-url": `${baseUrl}${route}` },
  });

  return await redirect(request, context);
}

For a successful redirect, the most important part of the response is the location header which contains the URL to redirect to. The headers field in the response is a union type, but since I know the exact type my function returns, I simplified the assertion code by always casting the value to that type:

const response = await invokeRedirect(route);

expect(response.status).toBe(301);
const location = (response.headers as Record<string, string> | undefined)
  ?.location;
expect(location).toBe(expected);

Since I want to test the function for many supported inputs, data-driven tests are a good fit:

test.each`
  route                   | expected
  ${"/categories/dotnet"} | ${"/tags/dotnet.html"}
  ${"/categories/csharp"} | ${"/tags/csharp.html"}
`(
  "redirects to $route -> $expected",
  async ({ route, expected }: { route: string; expected: string }) => {
    // ...
  },
);

To test the behavior for URLs without a matching redirect, I had to mock the fetch function which my function uses to read the response body:

const notFoundPageText = "404 Not Found";

global.fetch = jest.fn().mockResolvedValue({
  ok: true,
  status: 200,
  text: () => Promise.resolve(notFoundPageText),
});

To be as thorough as possible, I decided to also verify the fetch invocation, not only the response of the function under test:

expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith(`${baseUrl}/errors/404.html`);

expect(response.status).toBe(404);
expect(response.body).toBe(notFoundPageText);
const contentType = (response.headers as Record<string, string> | undefined)?.[
  "Content-Type"
];
expect(contentType).toBe("text/html");

To start each test with a clean state, I clear the mocks after every test:

afterEach(() => {
  jest.clearAllMocks();
});

You can find a sample project in my GitHub repository. I started off with the sample project from my previous blog post and added tests to it. The last commit adds all the tests as well as the extra setup to make them work.

Although the unit tests are great for thoroughly testing all the use cases for the Azure function, you shouldn't completely rely on them. Before deploying the function, it's a good idea to run it locally and invoke it using curl, for example:

curl -H "x-ms-original-url: http://localhost:7071/categories/dotnet" -v http://localhost:7071/api/redirect

This way the function will at least run in a simulated Azure Functions runtime, not only in a Node runtime provided by Jest.

Of course, the final and ultimate test will be when the Azure Function is deployed and invoked in Azure. You should have a small set of test cases prepared to run whenever you deploy any changes to your Azure Function. Only then can you be certain that it works as expected.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License