├── .eslintrc.yml ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.js ├── models │ └── product.js └── services │ └── product.js └── tests ├── db-handler.js ├── product-create.test.js └── product-getbyid.test.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | commonjs: true 3 | es6: true 4 | node: true 5 | jest: true 6 | extends: 7 | - standard 8 | globals: 9 | Atomics: readonly 10 | SharedArrayBuffer: readonly 11 | parserOptions: 12 | ecmaVersion: 2018 13 | rules: { 14 | indent: ['error', 4], 15 | semi: ['error', 'always'], 16 | curly: 0, 17 | 'brace-style': ['error', 'stroustrup'], 18 | 'eol-last': ['error', 'never'] 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A Node.js + Mongoose + Jest sample project that demonstrates **how to test mongoose operations using Jest with an in-memory database**. 2 | 3 | >This repo was build as an example for my article [Testing Node.js + Mongoose with an in-memory database](https://dev.to/paulasantamaria/testing-node-js-mongoose-with-an-in-memory-database-32np). 4 | 5 | # Dependencies 6 | What you need to run this project: 7 | - Node.js 8 | 9 | (MongoDB is not required because it'll run in memory, handled by the package `mongodb-memory-server`). 10 | 11 | # Try it out 12 | ## 1. Install dependencies 13 | ``` 14 | npm install 15 | ``` 16 | 17 | ## 2. Run tests 18 | ``` 19 | npm test 20 | ``` 21 | 22 | # Contribute 23 | Feel free to contribute to this project either by leaving your comments and suggestions in the Issues section or creating a PR. More and diverse test examples are always useful. Make sure to take a look at Jest docs and the existent examples to avoid repeating. 24 | 25 | # Tools 26 | Main tools used in this project: 27 | 28 | - [Mongoose](https://mongoosejs.com/) 29 | - [Jest](https://jestjs.io/) 30 | - [mongodb-memory-server package by @nodkz](https://github.com/nodkz/mongodb-memory-server) 31 | 32 | > Also take a look at [mongodb-memory-server-global](https://github.com/nodkz/mongodb-memory-server#mongodb-memory-server-global) to download mongod's binary globally and [mongodb-memory-server-core](https://github.com/nodkz/mongodb-memory-server#mongodb-memory-server-core) if you'll run the test on a server that already has mongod installed. 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-mongoose-inmemory", 3 | "version": "1.0.0", 4 | "description": "A sample project that demonstrates how to test mongoose operations through jest with an in-memory database.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "jest --runInBand ./test", 8 | "lint": "./node_modules/.bin/eslint ./", 9 | "lint-fix": "./node_modules/.bin/eslint ./ --fix" 10 | }, 11 | "keywords": [ 12 | "mongoose", 13 | "inmemory", 14 | "test", 15 | "jest", 16 | "mongodb" 17 | ], 18 | "author": "paula santamaría", 19 | "license": "ISC", 20 | "dependencies": { 21 | "mongoose": "^5.13.7" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^6.8.0", 25 | "eslint-config-standard": "^14.1.0", 26 | "eslint-plugin-import": "^2.20.0", 27 | "eslint-plugin-node": "^11.0.0", 28 | "eslint-plugin-promise": "^4.2.1", 29 | "eslint-plugin-standard": "^4.0.1", 30 | "jest": "^27.0.6", 31 | "mongodb-memory-server": "^7.3.6" 32 | }, 33 | "jest": { 34 | "testEnvironment": "node" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nothing to see here. 3 | * To test this project run `npm test`. 4 | */ -------------------------------------------------------------------------------- /src/models/product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | 5 | /** 6 | * Product model schema. 7 | */ 8 | const productSchema = new mongoose.Schema({ 9 | name: { type: String, required: true }, 10 | price: { type: Number, required: true }, 11 | description: { type: String } 12 | }); 13 | 14 | module.exports = mongoose.model('product', productSchema); -------------------------------------------------------------------------------- /src/services/product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const productModel = require('../models/product'); 4 | 5 | /** 6 | * Stores a new product into the database. 7 | * @param {Object} product product object to create. 8 | * @throws {Error} If the product is not provided. 9 | */ 10 | module.exports.create = async (product) => { 11 | if (!product) 12 | throw new Error('Missing product'); 13 | 14 | await productModel.create(product); 15 | }; 16 | 17 | /** 18 | * Retrieves a product by id. 19 | * @param {String} id Product unique identifier 20 | */ 21 | module.exports.getById = async (id) => { 22 | const product = await productModel.findById(id); 23 | return product; 24 | }; -------------------------------------------------------------------------------- /tests/db-handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | const { MongoMemoryServer } = require('mongodb-memory-server'); 5 | 6 | let mongod = undefined; 7 | 8 | 9 | /** 10 | * Connect to the in-memory database. 11 | */ 12 | module.exports.connect = async () => { 13 | mongod = await MongoMemoryServer.create(); 14 | const uri = mongod.getUri(); 15 | 16 | const mongooseOpts = { 17 | useNewUrlParser: true, 18 | autoReconnect: true, 19 | reconnectTries: Number.MAX_VALUE, 20 | reconnectInterval: 1000 21 | }; 22 | 23 | await mongoose.connect(uri, mongooseOpts); 24 | }; 25 | 26 | /** 27 | * Drop database, close the connection and stop mongod. 28 | */ 29 | module.exports.closeDatabase = async () => { 30 | if (mongod) { 31 | await mongoose.connection.dropDatabase(); 32 | await mongoose.connection.close(); 33 | await mongod.stop(); 34 | } 35 | }; 36 | 37 | /** 38 | * Remove all the data for all db collections. 39 | */ 40 | module.exports.clearDatabase = async () => { 41 | if (mongod) { 42 | const collections = mongoose.connection.collections; 43 | 44 | for (const key in collections) { 45 | const collection = collections[key]; 46 | await collection.deleteMany(); 47 | } 48 | } 49 | }; -------------------------------------------------------------------------------- /tests/product-create.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | 5 | const dbHandler = require('./db-handler'); 6 | const productService = require('../src/services/product'); 7 | const productModel = require('../src/models/product'); 8 | 9 | /** 10 | * Connect to a new in-memory database before running any tests. 11 | */ 12 | beforeAll(async () => { 13 | await dbHandler.connect(); 14 | }); 15 | 16 | /** 17 | * Clear all test data after every test. 18 | */ 19 | afterEach(async () => { 20 | await dbHandler.clearDatabase(); 21 | }); 22 | 23 | /** 24 | * Remove and close the db and server. 25 | */ 26 | afterAll(async () => { 27 | await dbHandler.closeDatabase(); 28 | }); 29 | 30 | /** 31 | * Product create test suite. 32 | */ 33 | describe('product create ', () => { 34 | /** 35 | * Tests that a valid product can be created through the productService without throwing any errors. 36 | */ 37 | it('can be created correctly', async () => { 38 | expect(async () => { 39 | await productService.create(productComplete); 40 | }) 41 | .not 42 | .toThrow(); 43 | }); 44 | 45 | /** 46 | * Tests that a product can be created without a description. 47 | */ 48 | it('can be created without description', async () => { 49 | expect(async () => { 50 | await productService.create(productMissingDescription); 51 | }) 52 | .not 53 | .toThrow(); 54 | }); 55 | 56 | /** 57 | * Product should exist after being created. 58 | */ 59 | it('exists after being created', async () => { 60 | await productService.create(productComplete); 61 | 62 | const createdProduct = await productModel.findOne(); 63 | 64 | expect(createdProduct.name) 65 | .toBe(productComplete.name); 66 | }); 67 | 68 | /** 69 | * Should throw an error when product doesn't have a name or price. 70 | */ 71 | it('requires name and price', async () => { 72 | await expect(productService.create(productMissingName)) 73 | .rejects 74 | .toThrow(mongoose.Error.ValidationError); 75 | 76 | await expect(productService.create(productMissingPrice)) 77 | .rejects 78 | .toThrow(mongoose.Error.ValidationError); 79 | }); 80 | }); 81 | 82 | const productComplete = { 83 | name: 'iPhone 11', 84 | price: 699, 85 | description: 'A new dual‑camera system captures more of what you see and love. ' 86 | }; 87 | 88 | const productMissingDescription = { 89 | name: 'iPhone 11', 90 | price: 699 91 | }; 92 | 93 | const productMissingName = { 94 | price: 699, 95 | description: 'A new dual‑camera system captures more of what you see and love. ' 96 | }; 97 | 98 | const productMissingPrice = { 99 | name: 'iPhone 11', 100 | description: 'A new dual‑camera system captures more of what you see and love. ' 101 | }; -------------------------------------------------------------------------------- /tests/product-getbyid.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | 5 | const dbHandler = require('./db-handler'); 6 | const productService = require('../src/services/product'); 7 | const productModel = require('../src/models/product'); 8 | 9 | /** 10 | * Connect to a new in-memory database before running any tests. 11 | */ 12 | beforeAll(async () => { 13 | await dbHandler.connect(); 14 | }); 15 | 16 | /** 17 | * Seed the database. 18 | */ 19 | beforeEach(async () => { 20 | await createProducts(); 21 | }); 22 | 23 | /** 24 | * Clear all test data after every test. 25 | */ 26 | afterEach(async () => { 27 | await dbHandler.clearDatabase(); 28 | }); 29 | 30 | /** 31 | * Remove and close the db and server. 32 | */ 33 | afterAll(async () => { 34 | await dbHandler.closeDatabase(); 35 | }); 36 | 37 | /** 38 | * Product getById test suite. 39 | */ 40 | describe('product getById ', () => { 41 | /** 42 | * Should return null if getById doesn't find any product with the provided id. 43 | */ 44 | it('should return null if nothing is found', async () => { 45 | await expect(productService.getById(mongoose.Types.ObjectId())) 46 | .resolves 47 | .toBeNull(); 48 | }); 49 | 50 | /** 51 | * Should return the correct product if getById finds the product with the provided id. 52 | */ 53 | it('should retrieve correct product if id matches', async () => { 54 | const foundProduct = await productService.getById(productIphoneId); 55 | 56 | expect(foundProduct.id).toBe(productIphoneId); 57 | expect(foundProduct.name).toBe(productIphone.name); 58 | }); 59 | }); 60 | 61 | /** 62 | * Seed the database with products. 63 | */ 64 | const createProducts = async () => { 65 | const createdIphone = await productModel.create(productIphone); 66 | productIphoneId = createdIphone.id; 67 | await productModel.create(productFitbit); 68 | }; 69 | 70 | let productIphoneId; 71 | 72 | const productIphone = { 73 | name: 'iPhone 11', 74 | price: 699, 75 | description: 'A new dual‑camera system captures more of what you see and love. ' 76 | }; 77 | 78 | const productFitbit = { 79 | name: 'Fitbit Inspire HR', 80 | price: 699, 81 | description: 'Get empowered to make a change and embrace your weight and fitness goals... ' 82 | }; --------------------------------------------------------------------------------