Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Testing your integration code for the Azure SDK for JavaScript is essential to ensure your applications interact correctly with Azure services. This guide shows you how to effectively test Azure SDK integration in your JavaScript applications a testing framework.
When deciding whether to mock out cloud service SDK calls or use a live service for testing purposes, it's important to consider the trade-offs between speed, reliability, and cost. This article demonstrates how to use a test framework for testing SDK integration. The application code inserts a document into Cosmos DB. The test code mocks out that resource usage so the cloud resource isn't used.
The frameworks used are:
- Jest with CommonJs
- Vitest with ESM
- Node.js Test runner with ESM
Prerequisites
Node.js LTS. LTS release status is "long-term support", which typically guarantees that critical bugs will be fixed for a total of 30 months.
The Node.js test runner is part of the Node.js installation.
Caution
The sample provided for the Node.js test runner uses the experimental node:test module with mock.fn(). Keep in mind that Node’s built‐in test runner doesn't yet offer a fully supported mocking API. Make sure that your target Node version supports the experimental APIs or consider using a third‑party mocking library (or stub functions) instead.
Mocking cloud services
Pros:
- Speeds up test suite by eliminating network latency.
- Provides predictable and controlled test environments.
- Easier to simulate various scenarios and edge cases.
- Reduces costs associated with using live cloud services, especially in continuous integration pipelines.
Cons:
- Mocks can drift from the actual SDK, leading to discrepancies.
- Might ignore certain features or behaviors of the live service.
- Less realistic environment compared to production.
Using a live service
Pros:
- Is a realistic environment that closely mirrors production?
- Is useful for integration tests to ensure different parts of the system work together?
- Is helpful to identify issues related to network reliability, service availability, and actual data handling?
Cons:
- Is slower due to network calls.
- Is more expensive due to potential service usage costs.
- Is complex and time-consuming to set up and maintain a live service environment that matches production.
The choice between mocking and using live services depends on your testing strategy. For unit tests where speed and control are paramount, mocking is often the better choice. For integration tests where realism is crucial, using a live service can provide more accurate results. Balancing these approaches helps achieve comprehensive test coverage while managing costs and maintaining test efficiency.
Test doubles: Mocks, stubs, and fakes
A test double is any kind of substitute used in place of something real for testing purposes. The type of double you choose is based on what you want it to replace. The term mock is often meant as any double when the term is used casually. In this article, the term is used specifically and illustrated specifically in the Jest test framework.
Mocks
Mocks (also called spies): Substitute in a function and be able to control and spy on the behavior of that function when it's called indirectly by some other code.
In the following examples, you have 2 functions:
- someTestFunction: The function you need to test. It calls a dependency,
dependencyFunction
, which you didn't write and don't need to test. - dependencyFunctionMock: Mock of the dependency.
import { mock } from 'node:test';
import assert from 'node:assert';
// ARRANGE
const dependencyFunctionMock = mock.fn();
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
assert.strictEqual(dependencyFunctionMock.mock.callCount(), 1);
The purpose of the test is to ensure that someTestFunction behaves correctly without actually invoking the dependency code. The test validates that the mock of the dependency was called.
Mock large versus small dependencies
When you decide to mock a dependency, you can choose to mock just what you need such as:
- A function or two from a larger dependency. Jest offers partial mocks for this purpose.
- All functions of a smaller dependency, as shown in the example in this article.
Stubs
The purpose of a stub is to replace a function's return data to simulate different scenarios. You use a stub to allow your code to call the function and receive various states, including successful results, failures, exceptions, and edge cases. State verification ensures your code handles these scenarios correctly.
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
// ARRANGE
const fakeDatabaseData = {first: 'John', last: 'Jones'};
const dependencyFunctionMock = mock.fn();
dependencyFunctionMock.mock.mockImplementation((arg) => {
return fakeDatabaseData;
});
// ACT
// Mock replaces the call to dependencyFunction with dependencyFunctionMock
const { name } = someTestFunction()
// ASSERT
assert.strictEqual(name, `${fakeDatabaseData.first} ${fakeDatabaseData.last}`);
The purpose of the preceding test is to ensure that the work done by someTestFunction
meets the expected outcome. In this simple example, the function's task is to concatenate the first and family names. By using fake data, you know the expected result and can validate that the function performs the work correctly.
Fakes
Fakes substitute a functionality that you wouldn't normally use in production, such as using an in-memory database instead of a cloud database.
// fake-in-mem-db.spec.ts
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
class FakeDatabase {
private data: Record<string, any>;
constructor() {
this.data = {};
}
save(key: string, value: any): void {
this.data[key] = value;
}
get(key: string): any {
return this.data[key];
}
}
// Function to test
function someTestFunction(db: FakeDatabase, key: string, value: any): any {
db.save(key, value);
return db.get(key);
}
describe('In-Mem DB', () => {
let fakeDb: FakeDatabase;
let testKey: string;
let testValue: any;
beforeEach(() => {
fakeDb = new FakeDatabase();
testKey = 'testKey';
testValue = {
first: 'John',
last: 'Jones',
lastUpdated: new Date().toISOString(),
};
});
afterEach(() => {
// Restore all mocks created by node:test’s mock helper.
mock.restoreAll();
});
it('should save and return the correct value', () => {
// Create a spy on the save method using node:test's mock helper.
const saveSpy = mock.method(fakeDb, 'save').mock;
// Call the function under test.
const result = someTestFunction(fakeDb, testKey, testValue);
// Verify state.
assert.deepStrictEqual(result, testValue);
assert.strictEqual(result.first, 'John');
assert.strictEqual(result.last, 'Jones');
assert.strictEqual(result.lastUpdated, testValue.lastUpdated);
// Verify behavior
assert.strictEqual(saveSpy.callCount(), 1);
const calls = saveSpy.calls;
assert.deepStrictEqual(calls[0].arguments, [testKey, testValue]);
});
});
The purpose of the preceding test is to ensure that someTestFunction
correctly interacts with the database. By using a fake in-memory database, you can test the function's logic without relying on a real database, making the tests faster and more reliable.
Scenario: Insert document into Cosmos DB using Azure SDK
Imagine you have an application that needs to write a new document to Cosmos DB if all the information is submitted and verified. If an empty form is submitted or the information doesn't match the expected format, the application shouldn't enter the data.
Cosmos DB is used as an example, however the concepts apply to most of the Azure SDKs for JavaScript. The following function captures this functionality:
// insertDocument.ts
import { Container } from '../data/connect-to-cosmos.js';
import type {
DbDocument,
DbError,
RawInput,
VerificationErrors,
} from '../data/model.js';
import Verify from '../data/verify.js';
export async function insertDocument(
container: Container,
doc: RawInput,
): Promise<DbDocument | DbError | VerificationErrors> {
const isVerified: boolean = Verify.inputVerified(doc);
if (!isVerified) {
return { message: 'Verification failed' } as VerificationErrors;
}
try {
const { resource } = await container.items.create({
id: doc.id,
name: `${doc.first} ${doc.last}`,
});
return resource as DbDocument;
} catch (error: any) {
if (error instanceof Error) {
if ((error as any).code === 409) {
return {
message: 'Insertion failed: Duplicate entry',
code: 409,
} as DbError;
}
return { message: error.message, code: (error as any).code } as DbError;
} else {
return { message: 'An unknown error occurred', code: 500 } as DbError;
}
}
}
Note
TypeScript types help define the kinds of data a function uses. While you don't need TypeScript to use Jest or other JavaScript testing frameworks, it's essential for writing type-safe JavaScript.
The functions in this application are:
Function | Description |
---|---|
insertDocument | Inserts a document into the database. This is what we want to test. |
inputVerified | Verifies the input data against a schema. Ensures data is in the correct format (for example, valid email addresses, correctly formatted URLs). |
cosmos.items.create | SDK function for Azure Cosmos DB using the @azure/cosmos. This is what we want to mock. It already has its own tests maintained by the package owners. We need to verify that the Cosmos DB function call was made and returned data if the incoming data passed verification. |
Install test framework dependency
This framework is provided as part of Node.js LTS.
Configure package to run test
Update the package.json
for the application with a new script to test our source code files. Source code files are defined by matching on partial file name and extension. Test runner looks for files following the common naming convention for test files: <file-name>.spec.[jt]s
. This pattern means files named like the following examples are interpreted as test files and run by Test runner:
- *.test.js: For example, math.test.js
- *.spec.js: For example, math.spec.js
- Files located in a tests directory, such as tests/math.js
Add a script to the package.json to support that test file pattern with Test runner:
"scripts": {
"test": "node --test --experimental-test-coverage --experimental-test-module-mocks --trace-exit"
}
Set up unit test for Azure SDK
How can we use mocks, stubs, and fakes to test the insertDocument function?
- Mocks: we need a mock to make sure the behavior of the function is tested such as:
- If the data does pass verification, the call to the Cosmos DB function happened only 1 time
- If the data doesn't pass verification, the call to the Cosmos DB function didn't happen
- Stubs:
- The data passed in matches the new document returned by the function.
When testing, think in terms of the test setup, the test itself, and the verification. In terms of test vernacular, this functionality uses the following terms:
- Arrange: set up your test conditions
- Act: call your function to test, also known as the system under test or SUT
- Assert: validate the results. Results can be behavior or state.
- Behavior indicates functionality in your test function, which can be verified. One example is that some dependency was called.
- State indicates the data returned from the function.
import { describe, it, afterEach, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
describe('boilerplate', () => {
beforeEach(() => {
// Setup required before each test
});
afterEach(() => {
// Cleanup required after each test
});
it('should <do something> if <situation is present>', async () => {
// Arrange
// - set up the test data and the expected result
// Act
// - call the function to test
// Assert
// - check the state: result returned from function
// - check the behavior: dependency function calls
});
});
When you use mocks in your tests, that template code needs to use mocking to test the function without calling the underlying dependency used in the function, such as the Azure client libraries.
Create the test file
The test file with mocks, to simulate a call to a dependency, has an extra setup.
There are several parts to the test file:
import
: The import statements allow you to use or mock out any of your test.mock
: Create the default mock behavior you want. Each test can alter as needed.describe
: Test group family for theinsert.ts
file.it
: Each test for theinsert.ts
file.
The test file covers three tests for the insert.ts
file, which can be divided into two validation types:
Validation type | Test |
---|---|
Happy path: should insert document successfully |
The mocked database method was called, and returned the altered data. |
Error path: should return verification error if input is not verified |
Data failed validation and returned an error. |
Error path:should return error if db insert fails |
The mocked database method was called, and returned an error. |
The following test file shows how to test the insertDocument function.
// insertDocument.test.ts
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
import { Container } from '../src/data/connect-to-cosmos.js';
import { createTestInputAndResult } from '../src/data/fake-data.js';
import type { DbDocument, DbError, RawInput } from '../src/data/model.js';
import { isDbError, isVerificationErrors } from '../src/data/model.js';
import Verify from '../src/data/verify.js';
import CosmosConnector from '../src/data/connect-to-cosmos.js';
import { insertDocument } from '../src/lib/insert.js';
describe('SDK', () => {
beforeEach(() => {
// Clear all mocks before each test
mock.restoreAll();
});
it('should return verification error if input is not verified', async () => {
const fakeContainer = {
items: {
create: async (_: any) => {
throw new Error('Create method not implemented');
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => false);
const mGetUniqueId = mock.method(CosmosConnector, 'getUniqueId').mock;
mGetUniqueId.mockImplementation(() => 'unique-id');
const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
// Arrange: wrong shape of document on purpose.
const doc = { name: 'test' } as unknown as RawInput;
// Act:
const insertDocumentResult = await insertDocument(fakeContainer, doc);
// Assert - State verification.
if (isVerificationErrors(insertDocumentResult)) {
assert.deepStrictEqual(insertDocumentResult, {
message: 'Verification failed',
});
} else {
throw new Error('Result is not of type VerificationErrors');
}
// Assert - Behavior verification: Verify that create was never called.
assert.strictEqual(mContainerCreate.callCount(), 0);
});
it('should insert document successfully', async () => {
// Arrange: override inputVerified to return true.
const { input, result }: { input: RawInput; result: Partial<DbDocument> } =
createTestInputAndResult();
const fakeContainer = {
items: {
create: async (doc: any) => {
return { resource: result };
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => true);
const mContainerCreate = mock.method(
fakeContainer.items as any,
'create',
).mock;
mContainerCreate.mockImplementation(async (doc: any) => {
return { resource: result };
});
// Act:
const receivedResult = await insertDocument(fakeContainer, input);
// Assert - State verification: Ensure the result is as expected.
assert.deepStrictEqual(receivedResult, result);
// Assert - Behavior verification: Ensure create was called once with correct arguments.
assert.strictEqual(mContainerCreate.callCount(), 1);
assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
id: input.id,
name: result.name,
});
});
it('should return error if db insert fails', async () => {
// Arrange: override inputVerified to return true.
const { input, result } = createTestInputAndResult();
const errorMessage: string = 'An unknown error occurred';
const fakeContainer = {
items: {
create: async (doc: any): Promise<any> => {
return Promise.resolve(null);
},
},
} as unknown as Container;
const mVerify = mock.method(Verify, 'inputVerified').mock;
mVerify.mockImplementation(() => true);
const mContainerCreate = mock.method(fakeContainer.items, 'create').mock;
mContainerCreate.mockImplementation(async (doc: any) => {
const mockError: DbError = {
message: errorMessage,
code: 500,
};
throw mockError;
});
// Act:
const insertDocumentResult = await insertDocument(fakeContainer, input);
// // Assert - Ensure create method was called once with the correct arguments.
assert.strictEqual(isDbError(insertDocumentResult), true);
assert.strictEqual(mContainerCreate.callCount(), 1);
assert.deepStrictEqual(mContainerCreate.calls[0].arguments[0], {
id: input.id,
name: result.name,
});
});
});
Troubleshooting
Most of the code in this article comes from the MicrosoftDocs/node-essentials GitHub repository. If you want to insert into a Cosmos DB Cloud resource, create the resource with this script.