46
loading...
This website collects cookies to deliver better user experience
import { MongoClient, MongoClientOptions, Collection, ObjectId } from 'mongodb';
export function createClient(url: string, options?: MongoClientOptions) {
return new MongoClient(url, options).connect();
}
export function createUserIndexes(client: MongoClient, database: string) {
return Promise.all([
client.db(database).createIndex('users', { email: 1 }, { unique: true }),
client.db(database).createIndex('users', { occupation: 1 })
]);
}
interface UserDTO {
_id: ObjectId;
name: string;
email: string;
age: number;
occupation: string;
timestamp: string;
}
export class UserService {
private collection: Collection;
constructor(private client: MongoClient, database: string) {
this.collection = this.client.db(database).collection('users');
}
createUser(user: Omit<UserDTO, 'timestamp' | '_id'>) {
return this.collection.insertOne({
...user,
timestamp: new Date().toISOString()
});
}
getUser(email: string) {
return this.collection.findOne<UserDTO>({ email });
}
getUsersByOccupation(occupation: string) {
return this.collection.find<UserDTO>({ occupation }).toArray();
}
updateUser(
email: string,
payload: Partial<Omit<UserDTO, 'timestamp' | '_id'>>
) {
return this.collection.updateOne({ email }, { $set: payload });
}
deleteUser(email: string) {
return this.collection.deleteOne({ email });
}
}
createClient
that initializes and returns a MongoClientcreateUserIndexes
that creates indexes for the users
collectionUserService
that contains methods for interacting with users
collection (create, delete, update user etc).insertOne
, a mock will emulate the functionality and spy on the arguments that the function was called with.createClient
has as arguments the host url
and the options
that the MongoClient
will be initialized with.export function createClient(url: string, options?: MongoClientOptions) {
return new MongoClient(url, options).connect();
}
jest.mock()
.jest.mock('mongodb');
describe('UserService', () => {
const {
constructorSpy,
collectionSpy,
createIndexSpy,
databaseSpy,
deleteOneSpy,
findSpy,
findOneSpy,
insertOneSpy,
updateOneSpy
}: MongodbSpies = jest.requireMock('mongodb');
beforeEach(() => {
constructorSpy.mockClear();
collectionSpy.mockClear();
createIndexSpy.mockClear();
databaseSpy.mockClear();
deleteOneSpy.mockClear();
findSpy.mockClear();
findOneSpy.mockClear();
insertOneSpy.mockClear();
updateOneSpy.mockClear();
});
...
});
monogdb
from the import { MongoClient } from 'mongodb'
with the mock you provide in __mocks__/mongodb.ts
. At jest.requireMock('mongodb');
I can get access to the spies that are specified in the mock and then use them inside our tests for asserting with what arguments the functions are getting called.__mocks__/mongodb.ts
:export const constructorSpy = jest.fn();
export class MongoClient {
constructor(url: string, options?: MongoClientOptions) {
constructorSpy(url, options);
}
async connect() {
return 'mock-client';
}
}
MongoClient
with a connect
methodconstructorSpy
, with constructorSpy
we can make sure that our constructor is called with the correct arguments.Mock functions or known as 'spies' let you spy on the behavior of a function call.
it('should connect and return a client', async () => {
const url = 'mongodb://localhost:27017';
const options = { keepAlive: true };
const client = await createClient(url, options);
expect(client).toBe('mock-client');
expect(constructorSpy).toHaveBeenCalledWith(url, options);
});
UserService
.createClient
is returning a string. That's wrong and can be misleading to someone reading the tests....
const client = await createClient(url, options);
expect(client).toBe('mock-client');
...
docker-compose.yaml
version: '3.9'
services:
mongodb:
image: mongo
ports:
- '27017:27017'
volumes:
- './seed.js:/docker-entrypoint-initdb.d/mongo-init.js:ro'
This docker-compose create a MongoDB service and attaches a seed file that runs on startup,
creates the needed collections and populates them with test data.
docker-compose up -d # -d (detach) is for running the service in the background
docker-compose down
beforeAll(async () => {
client = await createClient('mongodb://localhost:27017');
userService = new UserService(client, database);
});
afterAll(async () => {
await client.close();
});
beforeEach(async () => {
await client.db(database).collection('users').deleteMany({
name: 'test-user'
});
});
BeforeAll
tests create a client that connects to the docker-compose
MongoDB.AfterAll
tests close the connection to MongoDB.BeforeEach
test deletes the test-user
that was created during the tests, so each test is independent of previous data.it('should create needed indexes', async () => {
const indexes = await createUserIndexes(client, database);
expect(indexes).toEqual(['email_1', 'occupation_1']);
});
...
it('should return the correct user', async () => {
const user = await userService.getUser('[email protected]');
expect(user).toEqual({
_id: expect.any(ObjectId),
name: 'mock-chef',
email: '[email protected]',
age: 27,
occupation: 'chef',
timestamp: '2021-09-29T15:48:13.209Z'
});
});
UserService
and the MongoDB driver is being tested, meaning if a breaking change is introduced, tests can catch it.This package spins up an actual/real MongoDB server programmatically from within NodeJS, for testing or mocking during development.
beforeAll(async () => {
mongod = await MongoMemoryServer.create();
client = await createClient(mongod.getUri());
await seedData(client, seed, database, 'users');
userService = new UserService(client, database);
});
afterAll(async () => {
await client.close();
await mongod.stop();
});
beforeEach(async () => {
await client.db(database).collection('users').deleteMany({
name: 'test-user'
});
});
UserService
and the MongoDB driver is being tested.In-Memory Server
is that there is no option for seeding data at the start, rather the tests need to do it programmatically.In-Memory Server
.