NoSQL Databases in Modern Stacks
NoSQL databases trade the rigid schema and ACID guarantees of SQL databases for flexibility, scalability, and performance in specific use cases. As a QA engineer, you need different testing approaches for each type.
| Type | Examples | Use Case | Test Focus |
|---|---|---|---|
| Document | MongoDB, CouchDB | Flexible schemas, nested data | Schema consistency, indexing |
| Key-Value | Redis, Memcached | Caching, sessions, counters | TTL, eviction, data types |
| Wide-Column | DynamoDB, Cassandra | High-scale, time-series | Partition strategy, consistency |
| Graph | Neo4j, Neptune | Relationships, networks | Traversal correctness |
MongoDB Testing
MongoDB stores data as JSON-like documents (BSON). Collections do not enforce a schema by default, which means your application — and your tests — must validate data structure.
Schema Validation Testing
MongoDB supports optional schema validation rules:
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "email"],
properties: {
name: { bsonType: "string", minLength: 1 },
email: { bsonType: "string", pattern: "^.+@.+\\..+$" },
age: { bsonType: "int", minimum: 0, maximum: 150 }
}
}
}
});
Test cases:
// Valid document — should succeed
db.users.insertOne({ name: "Alice", email: "alice@test.com", age: 30 });
// Missing required field — should fail
db.users.insertOne({ name: "Bob" });
// Error: Document failed validation
// Invalid type — should fail
db.users.insertOne({ name: "Charlie", email: "charlie@test.com", age: "thirty" });
// Error: Document failed validation
Query Testing
// Test aggregation pipeline
const result = db.orders.aggregate([
{ $match: { status: "completed" } },
{ $group: { _id: "$userId", totalSpent: { $sum: "$total" } } },
{ $sort: { totalSpent: -1 } },
{ $limit: 10 }
]);
// Verify: top 10 users by spending, all orders completed
Index Testing
// Create index and verify usage
db.users.createIndex({ email: 1 }, { unique: true });
// Verify index is used
db.users.find({ email: "alice@test.com" }).explain("executionStats");
// Look for "IXSCAN" stage (not "COLLSCAN")
Redis Testing
Redis is an in-memory key-value store used for caching, sessions, rate limiting, and real-time features.
Data Type Testing
Redis supports multiple data types. Test that your application uses the correct type:
# String
SET user:123:name "Alice"
GET user:123:name # "Alice"
# Hash (object-like)
HSET user:123 name "Alice" email "alice@test.com" age "30"
HGETALL user:123 # Returns all fields
# List (ordered)
LPUSH notifications:123 "New order" "Payment received"
LRANGE notifications:123 0 -1 # Returns all items
# Set (unique values)
SADD user:123:roles "admin" "editor" "admin"
SMEMBERS user:123:roles # {"admin", "editor"} — no duplicate
# Sorted Set (ranked)
ZADD leaderboard 100 "Alice" 200 "Bob" 150 "Charlie"
ZREVRANGE leaderboard 0 2 WITHSCORES # Bob(200), Charlie(150), Alice(100)
TTL Testing
# Set key with 60-second TTL
SET session:abc123 "user-data" EX 60
# Verify TTL is set
TTL session:abc123 # Returns remaining seconds (e.g., 59)
# Verify key exists
EXISTS session:abc123 # 1 (exists)
# After 60 seconds...
EXISTS session:abc123 # 0 (expired and deleted)
Cache Invalidation Testing
Cache testing is notoriously tricky. Test these scenarios:
- Cache hit: Data is in Redis, application returns cached value.
- Cache miss: Data is not in Redis, application queries the database and caches the result.
- Cache invalidation: Data is updated in the database, cached value is invalidated.
- Stale cache: After invalidation, the next request returns fresh data.
DynamoDB Testing
DynamoDB is a fully managed wide-column store by AWS. Its testing requires understanding partition keys, sort keys, and eventually consistent reads.
Key Design Testing
import boto3
dynamodb = boto3.resource('dynamodb', endpoint_url='http://localhost:8000')
table = dynamodb.Table('Orders')
# Insert with partition key (userId) and sort key (orderId)
table.put_item(Item={
'userId': 'user-123',
'orderId': 'order-456',
'total': 99.99,
'status': 'pending'
})
# Query by partition key (efficient)
response = table.query(
KeyConditionExpression='userId = :uid',
ExpressionAttributeValues={':uid': 'user-123'}
)
assert len(response['Items']) >= 1
# Query with sort key range (get orders between dates)
response = table.query(
KeyConditionExpression='userId = :uid AND orderId BETWEEN :start AND :end',
ExpressionAttributeValues={
':uid': 'user-123',
':start': 'order-001',
':end': 'order-999'
}
)
Consistency Testing
DynamoDB offers eventually consistent reads (default) and strongly consistent reads:
# Eventually consistent read (may return stale data)
response = table.get_item(Key={'userId': 'user-123', 'orderId': 'order-456'})
# Strongly consistent read (always returns latest)
response = table.get_item(
Key={'userId': 'user-123', 'orderId': 'order-456'},
ConsistentRead=True
)
Exercise: NoSQL Testing Lab
Part 1: MongoDB Testing
Start MongoDB with Docker:
docker run -d --name mongo-test -p 27017:27017 mongo:7
Task 1.1: Create a collection with schema validation for a “products” collection. Test that invalid documents are rejected.
Task 1.2: Insert 1,000 products with various categories. Write aggregation queries to calculate average price per category and verify the results.
Task 1.3: Create a compound index on {category: 1, price: -1}. Verify with explain() that queries use the index.
Part 2: Redis Testing
Start Redis:
docker run -d --name redis-test -p 6379:6379 redis:7
Task 2.1: Implement and test a session store:
- Create a session with 300-second TTL.
- Verify the session data is retrievable.
- Verify the TTL is counting down.
- Wait for expiration and verify the session is gone.
Task 2.2: Implement and test a rate limiter:
- Allow 10 requests per minute per user.
- Send 10 requests — all should succeed.
- Send an 11th request — it should be rejected.
- Wait 60 seconds and verify the limit resets.
Task 2.3: Test cache invalidation:
- Cache a user profile in Redis.
- Update the user profile in your “database” (another Redis key simulating a DB).
- Invalidate the cache.
- Verify the next read fetches from the “database” and updates the cache.
Part 3: DynamoDB Testing
Use DynamoDB Local:
docker run -d --name dynamo-test -p 8000:8000 amazon/dynamodb-local
Task 3.1: Create an Orders table with userId (partition key) and orderId (sort key). Insert 50 orders across 5 users and test query patterns.
Task 3.2: Test conditional writes — update an order’s status only if it is currently “pending”. Verify the condition prevents updating a “shipped” order.
Deliverables
- MongoDB: Schema validation tests, aggregation verification, index usage proof.
- Redis: Session TTL tests, rate limiter tests, cache invalidation tests.
- DynamoDB: Query pattern tests, conditional write tests.
- A comparison summary of testing differences between SQL and NoSQL.