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.

TypeExamplesUse CaseTest Focus
DocumentMongoDB, CouchDBFlexible schemas, nested dataSchema consistency, indexing
Key-ValueRedis, MemcachedCaching, sessions, countersTTL, eviction, data types
Wide-ColumnDynamoDB, CassandraHigh-scale, time-seriesPartition strategy, consistency
GraphNeo4j, NeptuneRelationships, networksTraversal 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:

  1. Cache hit: Data is in Redis, application returns cached value.
  2. Cache miss: Data is not in Redis, application queries the database and caches the result.
  3. Cache invalidation: Data is updated in the database, cached value is invalidated.
  4. 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:

  1. Create a session with 300-second TTL.
  2. Verify the session data is retrievable.
  3. Verify the TTL is counting down.
  4. Wait for expiration and verify the session is gone.

Task 2.2: Implement and test a rate limiter:

  1. Allow 10 requests per minute per user.
  2. Send 10 requests — all should succeed.
  3. Send an 11th request — it should be rejected.
  4. Wait 60 seconds and verify the limit resets.

Task 2.3: Test cache invalidation:

  1. Cache a user profile in Redis.
  2. Update the user profile in your “database” (another Redis key simulating a DB).
  3. Invalidate the cache.
  4. 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

  1. MongoDB: Schema validation tests, aggregation verification, index usage proof.
  2. Redis: Session TTL tests, rate limiter tests, cache invalidation tests.
  3. DynamoDB: Query pattern tests, conditional write tests.
  4. A comparison summary of testing differences between SQL and NoSQL.