58MongoDB (NoSQL testing)
What you will master here
- Document model vs relational
- BSON, _id, ObjectId
- CRUD: insertOne/Many, find, updateOne/Many, deleteOne/Many
- Query operators: $eq/$gt/$in/$regex/$exists
- Update operators: $set/$inc/$push/$pull
- Aggregation pipeline
- Indexes (single, compound, text, TTL)
- Transactions, replica sets, sharding (overview)
- Testing with mongo-memory-server / Testcontainers
58.1 Document model
{
"_id": ObjectId("..."),
"email": "alice@test.com",
"name": "Alice",
"addresses": [
{ "city": "BLR", "zip": "560001" },
{ "city": "BOM", "zip": "400001" }
],
"createdAt": ISODate("2026-06-19T00:00:00Z")
}
Schemaless by default. Documents in the same collection can have different shapes. Schema validation can be enforced via JSON Schema on the collection.
58.2 CRUD essentials
// Connect
const { MongoClient } = require('mongodb');
const client = await MongoClient.connect('mongodb://localhost:27017');
const db = client.db('app');
// Insert
await db.collection('users').insertOne({ email: 'a@test.com', name: 'Alice' });
await db.collection('users').insertMany([{...}, {...}]);
// Find
await db.collection('users').findOne({ email: 'a@test.com' });
const users = await db.collection('users')
.find({ active: true, age: { $gt: 18 } })
.sort({ createdAt: -1 })
.limit(20)
.toArray();
// Update
await db.collection('users').updateOne(
{ _id }, { $set: { name: 'Alice S.' }, $inc: { loginCount: 1 } }
);
// Delete
await db.collection('users').deleteOne({ _id });
await db.collection('users').deleteMany({ active: false });
58.3 Query operators
| Operator | Meaning |
|---|---|
| $eq, $ne | equal / not equal |
| $gt, $gte, $lt, $lte | comparison |
| $in, $nin | in/not in array |
| $exists | field present |
| $regex | regex match |
| $and, $or, $not, $nor | logical |
| $elemMatch | array element matches all conditions |
| $size | array length |
58.4 Aggregation pipeline
// Total spend per user with active orders
db.collection('orders').aggregate([
{ $match: { status: 'paid' } },
{ $group: { _id: '$userId', total: { $sum: '$amount' }, count: { $sum: 1 } } },
{ $sort: { total: -1 } },
{ $limit: 10 },
{ $lookup: {
from: 'users',
localField: '_id',
foreignField: '_id',
as: 'user',
}},
{ $project: { total: 1, count: 1, email: { $first: '$user.email' } } },
]).toArray();
58.5 Indexes
await db.collection('users').createIndex({ email: 1 }, { unique: true });
await db.collection('orders').createIndex({ userId: 1, createdAt: -1 }); // compound
await db.collection('posts').createIndex({ title: 'text', body: 'text' }); // full-text
await db.collection('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); // TTL
Explain
db.collection('users').find({ email: 'x' }).explain('executionStats');
// stage = IXSCAN → uses index. COLLSCAN → full scan, slow on large data.
58.6 Transactions
const session = client.startSession();
try {
await session.withTransaction(async () => {
await db.collection('orders').insertOne({...}, { session });
await db.collection('inventory').updateOne({ sku }, { $inc: { qty: -1 }}, { session });
});
} finally { await session.endSession(); }
Requires replica set or sharded cluster. Default single-node MongoDB doesn't support transactions.
58.7 Testing patterns
- mongo-memory-server — spawns ephemeral in-memory MongoDB per test. Fast, no Docker needed.
- Testcontainers — real MongoDB in Docker per suite. Higher fidelity.
- Cleanup: drop collections in afterEach, or per-test database.
import { MongoMemoryServer } from 'mongodb-memory-server';
let mongo: MongoMemoryServer;
beforeAll(async () => {
mongo = await MongoMemoryServer.create();
process.env.MONGO_URL = mongo.getUri();
});
afterAll(async () => mongo.stop());
58.8 SQL → MongoDB mapping
| SQL | MongoDB |
|---|---|
| Database | Database |
| Table | Collection |
| Row | Document |
| Column | Field |
| JOIN | $lookup (in aggregation) |
| Primary key | _id (ObjectId default) |
| Index | Index |
| Transaction | Multi-doc transaction (replica set) |
Module 63 — MongoDB Q&A
Document vs relational model?
Relational: rigid schema, normalised tables, joins. Document: flexible schema per collection, nested arrays/objects allowed, joins are explicit ($lookup) and discouraged. Document suits read-heavy, denormalised data; relational suits transactional integrity.
What's an ObjectId?
12-byte default _id: 4 bytes timestamp + 5 random + 3 counter. Time-orderable, unique without coordination. Looks like
ObjectId('6650...').How do you find slow queries?
.explain('executionStats') shows the plan. Look for COLLSCAN (bad — full scan) vs IXSCAN (good — uses index). Mongo profiler logs slow queries above a threshold.How do you JOIN in MongoDB?
$lookup in the aggregation pipeline. Optionally followed by $unwind to flatten array results. Less efficient than RDBMS joins; design for denormalised reads.
When do you need a transaction?
When multiple documents must update atomically — e.g. decrement inventory + create order. Single-document operations are already atomic. Transactions need a replica set or sharded cluster.
What is a TTL index?
An index with expireAfterSeconds — Mongo automatically deletes documents whose indexed field is older than the threshold. Used for sessions, OTP codes, cache.
Compound index — does order matter?
Yes. Mongo uses a compound index left-to-right (prefix rule). An index on
{ a: 1, b: 1 } helps queries on { a } and { a, b } but NOT { b } alone.How do you write fast Mongo tests?
mongodb-memory-server spawns an in-memory mongod per suite — boots in <1s, no Docker. Per-test cleanup drops collections. For higher fidelity (transactions, replica-set features), use Testcontainers.
What's the aggregation pipeline?
Sequence of stages ($match, $group, $project, $lookup, $sort, $limit, etc.) that transform documents. Each stage feeds into the next. Equivalent of SQL GROUP BY + JOIN + computed fields but in a pipeline DSL.