Sabbagh's Blagh
The Software blog of a Development Journeyman

Unit Test Azure Function Apps with Handwritten Mocks

May 27, 2023

tagged: azure, unit testing, handwritten, function, jest

Mimicry Thanks to Worachat Sosdri for making this photo available freely on unsplash 🎁


I wrote previously about setting up jest with typescript on an express backend. Recently, I retrofitted one of our long-running, not-often-touched function apps with some sweet unit tests.

The app is a typescript function, triggered off of an Azure Storage Queue, and simply processes and upserts an array of Operation records to Cosmos.


The approach was pretty simple, consisting of three pieces:

  • doing some light refactoring of the app code for testability (and readibilty),
  • using a pretty sweet npm helper package to configure the function app's runtime, and
  • delving into Cosmos's type definitions and stitching together a handwritten mock.

My hope for this post is two-fold:

  • to give you a template for handwriting mocks, when you need to, and
  • to expose you to the stub-azure-function-context npm package for unit testing Azure Function Apps.

Some Light Refactoring 🚧

I won't go into depth on the refactor since it was pretty straightforward.

The single exported function's pseudocode looked like:

const ingestThings = (Thing[]) => {
  // dedupe and group all the thing items

  // iterate through groupedThings, creating either a PATCH or POST for each
}

The obvious thing was to break these up into two functions:



const dedupeAndGroup();

const ingestThings = (Thing[]) => {
  const groupedThings = dedupeAndGroup();

  // iterate through groupedThings, creating either a PATCH or POST for each
}

This allowed me to test both dedupeAndGroup and ingestThings independently.

If you're working with a function app that processes arrays you might have a similar optimization to make, if you haven't done so already.


Mocking Cosmos 🪐

@azure/cosmos exposes a Container class through which you interact with, well, a Cosmos container.

If you remember your Cosmos NoSQL hierarchy, you'll recall that the container is inside a database - the database handle you retrieve by instantiating a CosmosClient[www.cosmos.com]. This requires several chained calls, which look like this:

import { Container, CosmosClient, Database } from "@azure/cosmos"

const thingsContainer = new CosmosClient({
  endpoint: endpoint, 
  key: key
})
.database(databaseName)
.container(thingsContainerName);

If we want to unit test our ingestThings, we'll need to mock out this cosmos container so that a) we're not reaching out to an actual database in our unit test, and b) so that we can assert against it in our test.


I chose to mock and not stub, the difference being that you don't assert against stubs (at least for die-hard TAOUT followers). And in Jest, you can spy on mocks.


In order to mock the container, in our app code we wrapped the nested calls above into a helper method, getThingsContainer. And in our unit test, we used Jest's mockReturnValue as the seam through which to insert our handwritten mock.


// we needed to import all the named exports here so that we can provide an object to jest's spyOn function.
import * as cosmosHelperLib from '../shared/cosmos';

...

jest.spyOn(cosmosHelperLib, 'getThingsContainer').mockReturnValue(({
  // handwritten mock goes here; we need to return a Container-shaped mock
}))

Thanks to this blog post from Chak Shun Yu for the glob import hint, by the way.


What does this Container class look like? We can look at the type definitions in @azure/cosmos to discern the shape to which it needs to conform. The whole class is pretty large, so we'll restrict our mock to include just the properties and methods that our code touches.


Our app code makes these method calls on Container:


await container.item(thing.id, thing.id).read<Thing>()

...

await container.items.bulk(cosmosOperations, { continueOnError: true });


In the Container definition we see the method signatures:


// cosmos.d.ts in @azure/cosmos

export declare class Container {
  readonly items: Items;

  ...

  items(): Items;

  ...

  item(id: string, partitionKeyValue?: PartitionKey): Item;

}


Both of these return yet other nested classes, Item and Items, so our mock will need to handle those too. The type definitions for those two classes are below (again, only looking at the relevant portions for our app code):



export declare class Item {

  read<T extends ItemDefinition = any>(options?: RequestOptions): Promise<ItemResponse<T>>;

}

...

export declare class Items {

  bulk(operations: OperationInput[], bulkOptions?: BulkOptions, options?: RequestOptions): Promise<OperationResponse[]>

}


Now we can realize the structure of our handwritten mock:



jest.spyOn(sharedCosmosLib, 'getThingsContainer').mockReturnValue(({
  item: (id: string, partitionKey: string) => {
    return {
      read: () => {
        return {};
      }
    }
  },
  items: {
    bulk: (operations: any[], bulkOptions: any) => {
      return [];
    }
  }
} as unknown) as Container);


You'll notice that this mock is only returning an empty object and array. What it returns will depend on your app code and how you want to test your function.


For my purposes, I want to ensure that the methods I've mocked (container.item.read and container.items.bulk) are called with certain arguments. Jest has a matcher just for that, expect().toHaveBeenCalledWith. I plan to assert against the items.bulk method of our handwritten mock.


But that's beyond the scope of this post, so I'll save it for later.


NPM Sweetness 🍫

To run the function app as it'll be run in Azure, we need to stub in a Function Context. This npm package creates the context for you - stub-azure-function-context.

Here's a sample use case for setting up the context for a Queue binding:


await functionRunner(
  functionName,
  [{
    name: 'queueTriggerBinding',
    direction: 'in',
    type: 'queueTrigger'
  }],
  {
    queueTrigerBinding: QueueBinding.createFromMessageText(anything),
  }
);


If you're using the Arrange-Act-Assert pattern in your tests (you should be), this would be the Act step, as functionRunner runs the function. After which you can assert as you usually would.


That's it. Hope this was helpful!

© Copyright 2024 Sabbagh's Blagh. Powered with by CreativeDesignsGuru