TLDR; the 2nd code block is how you do it.
I updated a service method such that it created a Mongo ID before sending it to the DB instead of relying on the DB to do it for me. There are a few reasons why you might want to do this, in my case, it was because I was linking 2 things together, so instead of running 2 mutations (the 1st to create the linked item and get back the created ID, and the 2nd to add the new ID to the item I was linking from), I created the new IDs in my method, linked the 2 together and sent them off on their merry way in one happy mutation.
I needed to write a unit test that ensured the created ID was correctly added as the ID on the linked item as well as the item it was linked from, but without mocking, we would have no way of knowing what ID would be created and so could not check that the correct ID existed in the right places.
Simplified, here's the code we're testing:
import { ObjectId } from 'mongodb';
type LinkedItem = { itemId: string };
type Item {
id?: string; // MongoId
newItemId: string; // temp ID used to identify new items that don't yet have an ID
contents: Whatever;
linkedItems?: Array<LinkedItem>;
}
const saveItemsToDb = (inputs: Array<Item>): Promise<void> {
// Create Mongo ID if one doesn't exist
const inputsWithIds = inputs.map(input =>
input.id
? input
: { ...input, id: new ObjectId().toString() }
// Update the linked items to include the new IDs
const inputsWithUpdatedLinks = inputsWithIds.map(input => {
if (!input.linkedItems || !input.linkedItems.length) {
return input;
}
const updatedLinkedItems: Array<LinkedItem> = [];
input.linkedItems.forEach(linkedItemInput => {
const linkedItem = inputsWithIds.find(
inputWithId =>
inputWithId.id === linkedItemInput.itemId ||
inputWithId.newItemId === linkedItemInput.itemId
);
assert(linkedItem);
assert(linkedItem.id);
updatedLinkedItems.push({ itemId: linkedItem.id });
})
// Return the input with the updated links
return {
...input,
linkedItems: updatedLinkedItems,
}
})
// Save the updated inputs to the DB
await db.save(inputsWithUpdatedLinks)
}
And here's a simplified section of the unit tests with the mocks:
import { ObjectId } from 'mongodb';
import {
NEW_ID_1,
NEW_ID_2,
mockItemsToSave,
mockSavedItems
} from './testData';
describe('Item service', () => {
beforeEach(() => {
jest
.spyOn(ObjectId.prototype, 'toString')
.mockReturnValueOnce(NEW_ID_1)
.mockReturnValueOnce(NEW_ID_2);
})
afterEach(() => {
jest.restoreAllMocks();
})
it('links new items', async () => {
await saveItemsToDb(mockItemsToSave);
expect(mockDb.save).toHaveBeenCalledWith(mockSavedItems);
});
});
By using jest.spyOn()
we can watch for when ObjectId().toString()
gets called and override its return value so that we're able to test what happens with our data. By using a chain of .mockReturnValueOnce()
methods, we're also able to dictate what the method returns when it's called multiple times. For example, in the context of our code, if we're adding multiple new items that all link to each other, we can easily check that the right item links are being set across multiple new items.
It's also important to note that we're calling jest.spyOn()
from within the beforeEach
callback and as such, we need to call jest.restoreAllMocks()
from our afterEach
callback function so that our mock return values (NEW_ID_1
and NEW_ID_2
) get reset each time we run a test.
Hope that helps! I'm also leaving this here to help me when I inevitably forget how I did this in the future.
Top comments (0)