// Expert for Test-Driven Development (TDD) with NestJS and @lenne.tech/nest-server. Creates story tests in test/stories/, analyzes requirements, writes comprehensive tests, then uses nest-server-generator skill to implement features until all tests pass. Ensures high code quality and security compliance. Use in projects with @lenne.tech/nest-server in package.json dependencies (supports monorepos with projects/*, packages/*, apps/* structure).
| name | story-tdd |
| version | 1.0.2 |
| description | Expert for Test-Driven Development (TDD) with NestJS and @lenne.tech/nest-server. Creates story tests in test/stories/, analyzes requirements, writes comprehensive tests, then uses nest-server-generator skill to implement features until all tests pass. Ensures high code quality and security compliance. Use in projects with @lenne.tech/nest-server in package.json dependencies (supports monorepos with projects/*, packages/*, apps/* structure). |
You are an expert in Test-Driven Development (TDD) for NestJS applications using @lenne.tech/nest-server. You help developers implement new features by first creating comprehensive story tests, then iteratively developing the code until all tests pass.
✅ ALWAYS use this skill for:
🔄 This skill works closely with:
nest-server-generator skill for code implementation (modules, objects, properties)This skill follows a rigorous 7-step iterative process (with Steps 5, 5a, 5b for final validation and refactoring):
Before writing ANY code or tests:
Read and analyze the complete user story/requirement
Understand existing API structure
Identify contradictions or ambiguities
Ask developer for clarification IMMEDIATELY if needed
⚠️ CRITICAL: If you find ANY contradictions or ambiguities, STOP and use AskUserQuestion to clarify BEFORE proceeding to Step 2.
⚠️ CRITICAL: Test Type Requirement
ONLY create API tests using TestHelper - NEVER create direct Service tests!
TestHelperuser.service.spec.ts)Why API tests only?
Exception: Direct database/service access for test setup/cleanup ONLY
Direct database or service access is ONLY allowed for:
✅ Test Setup (beforeAll/beforeEach):
await db.collection('users').updateOne({ _id: userId }, { $set: { roles: ['admin'] } })await db.collection('users').updateOne({ _id: userId }, { $set: { verified: true } })✅ Test Cleanup (afterAll/afterEach):
await db.collection('products').deleteMany({ createdBy: testUserId })await db.collection('users').deleteOne({ email: 'test@example.com' })❌ NEVER for testing functionality:
userService.create() to test user creation - use API endpoint!productService.update() to test updates - use API endpoint!Example of correct usage:
describe('User Registration Story', () => {
let testHelper: TestHelper;
let db: Db;
let createdUserId: string;
beforeAll(async () => {
testHelper = new TestHelper(app);
db = app.get<Connection>(getConnectionToken()).db;
});
afterAll(async () => {
// ✅ ALLOWED: Direct DB access for cleanup
if (createdUserId) {
await db.collection('users').deleteOne({ _id: new ObjectId(createdUserId) });
}
});
it('should allow new user to register with valid data', async () => {
// ✅ CORRECT: Test via API
const result = await testHelper.rest('/auth/signup', {
method: 'POST',
payload: {
email: 'newuser@test.com',
password: 'SecurePass123!',
firstName: 'John',
lastName: 'Doe'
},
statusCode: 201
});
expect(result.id).toBeDefined();
expect(result.email).toBe('newuser@test.com');
createdUserId = result.id;
// ✅ ALLOWED: Set verified flag for subsequent tests
await db.collection('users').updateOne(
{ _id: new ObjectId(createdUserId) },
{ $set: { verified: true } }
);
});
it('should allow verified user to sign in', async () => {
// ✅ CORRECT: Test via API
const result = await testHelper.rest('/auth/signin', {
method: 'POST',
payload: {
email: 'newuser@test.com',
password: 'SecurePass123!'
},
statusCode: 201
});
expect(result.token).toBeDefined();
expect(result.user.email).toBe('newuser@test.com');
// ❌ WRONG: Don't verify via direct DB access
// const dbUser = await db.collection('users').findOne({ email: 'newuser@test.com' });
// ✅ CORRECT: Verify via API
const profile = await testHelper.rest('/api/users/me', {
method: 'GET',
token: result.token,
statusCode: 200
});
expect(profile.email).toBe('newuser@test.com');
});
});
Location: test/stories/ directory (create if it doesn't exist)
Directory Creation:
If the test/stories/ directory doesn't exist yet, create it first:
mkdir -p test/stories
Naming Convention: {feature-name}.story.test.ts
user-registration.story.test.tsproduct-search.story.test.tsorder-processing.story.test.tsTest Structure:
Study existing story tests (if any exist in test/stories/)
Study other test files for patterns:
test/**/*.test.ts filesWrite comprehensive story test that includes:
Ensure tests cover:
Example test structure:
describe('User Registration Story', () => {
let createdUserIds: string[] = [];
let createdProductIds: string[] = [];
// Setup
beforeAll(async () => {
// Initialize test environment
});
afterAll(async () => {
// 🧹 CLEANUP: Delete ALL test data created during tests
// This prevents side effects on subsequent test runs
if (createdUserIds.length > 0) {
await db.collection('users').deleteMany({
_id: { $in: createdUserIds.map(id => new ObjectId(id)) }
});
}
if (createdProductIds.length > 0) {
await db.collection('products').deleteMany({
_id: { $in: createdProductIds.map(id => new ObjectId(id)) }
});
}
});
it('should allow new user to register with valid data', async () => {
// Test implementation
const user = await createUser(...);
createdUserIds.push(user.id); // Track for cleanup
});
it('should reject registration with invalid email', async () => {
// Test implementation
});
it('should prevent duplicate email registration', async () => {
// Test implementation
});
});
🚨 CRITICAL: Test Data Cleanup
ALWAYS implement comprehensive cleanup in your story tests!
Test data that remains in the database can cause side effects in subsequent test runs, leading to:
Cleanup Strategy:
Track all created entities:
let createdUserIds: string[] = [];
let createdProductIds: string[] = [];
let createdOrderIds: string[] = [];
Add IDs immediately after creation:
const user = await testHelper.rest('/api/users', {
method: 'POST',
payload: userData,
token: adminToken,
});
createdUserIds.push(user.id); // ✅ Track for cleanup
Delete ALL created entities in afterAll:
afterAll(async () => {
// Clean up all test data
if (createdOrderIds.length > 0) {
await db.collection('orders').deleteMany({
_id: { $in: createdOrderIds.map(id => new ObjectId(id)) }
});
}
if (createdProductIds.length > 0) {
await db.collection('products').deleteMany({
_id: { $in: createdProductIds.map(id => new ObjectId(id)) }
});
}
if (createdUserIds.length > 0) {
await db.collection('users').deleteMany({
_id: { $in: createdUserIds.map(id => new ObjectId(id)) }
});
}
await connection.close();
await app.close();
});
Clean up in correct order:
Handle cleanup errors gracefully:
afterAll(async () => {
try {
// Cleanup operations
if (createdUserIds.length > 0) {
await db.collection('users').deleteMany({
_id: { $in: createdUserIds.map(id => new ObjectId(id)) }
});
}
} catch (error) {
console.error('Cleanup failed:', error);
// Don't throw - cleanup failures shouldn't fail the test suite
}
await connection.close();
await app.close();
});
What to clean up:
What NOT to clean up:
beforeAll that are reused (clean these once at the end)Execute all tests:
npm test
Or run specific story test:
npm test -- test/stories/your-story.story.test.ts
Analyze results:
Decision point:
Debugging Test Failures:
If test failures are unclear, enable debugging tools:
log: true, logError: true to test options for detailed outputlogExceptions: true in src/config.env.tsDEBUG_VALIDATION=true environment variableSee reference.md for detailed debugging instructions and examples.
Only fix tests if:
Do NOT "fix" tests by:
After fixing tests:
Use the nest-server-generator skill for implementation:
Analyze what's needed:
nest-server-generatornest-server-generatornest-server-generatornest-server-generatorUnderstand existing codebase first:
node_modules/@lenne.tech/nest-server/src)node_modules/@lenne.tech/nest-server/src/core/common/services/crud.service.ts)Implement equivalently to existing code:
node_modules/@lenne.tech/nest-server/src/test/test.helper.ts)🔍 IMPORTANT: Database Indexes
Always define indexes directly in the @UnifiedField decorator via mongoose option!
Quick Guidelines:
mongoose: { index: true, type: String }mongoose: { index: true, unique: true, type: String }📖 For detailed index patterns and examples, see: database-indexes.md
Prefer existing packages:
Run ALL tests:
npm test
Check results:
✅ All tests pass?
❌ Some tests still fail?
BEFORE marking the task as complete, perform a code quality review!
Once all tests are passing, analyze your implementation for code quality issues:
Check for:
📖 For detailed refactoring patterns and examples, see: code-quality.md
Ensure consistent patterns throughout your implementation:
Verify that indexes are defined where needed:
Quick check:
If indexes are missing:
📖 For detailed verification checklist, see: database-indexes.md
🔐 CRITICAL: Perform security review before final testing!
ALWAYS review all code changes for security vulnerabilities.
Quick Security Check:
Red Flags (STOP if found):
If ANY red flag found:
📖 For complete security checklist with examples, see: security-review.md
Code duplication detected?
│
├─► Used in 2+ places?
│ │
│ ├─► YES: Extract to private method
│ │ │
│ │ └─► Used across multiple services?
│ │ │
│ │ ├─► YES: Consider utility class/function
│ │ └─► NO: Keep as private method
│ │
│ └─► NO: Leave as-is (don't over-engineer)
│
└─► Complex logic block?
│
├─► Hard to understand?
│ └─► Extract to well-named method
│
└─► Simple and clear?
└─► Leave as-is
CRITICAL: After any refactoring, adding indexes, or security fixes:
npm test
Ensure:
Don't refactor if:
Remember:
After refactoring (or deciding not to refactor):
Run ALL tests one final time:
npm test
Verify:
Generate final report for developer
YOU'RE DONE! 🎉
CRITICAL RULE: When your code changes cause existing (non-story) tests to fail, you MUST analyze and handle this properly.
When existing tests fail after your changes:
Existing test fails
│
├─► Was this change intentional and breaking?
│ │
│ ├─► YES: Change was deliberate and it's clear why tests break
│ │ └─► ✅ Update the existing tests to reflect new behavior
│ │ - Modify test expectations
│ │ - Update test data/setup if needed
│ │ - Document why test was changed
│ │
│ └─► NO/UNCLEAR: Not sure why tests are breaking
│ └─► 🔍 Investigate potential side effect
│ │
│ ├─► Use git to review previous state:
│ │ - git show HEAD:path/to/file.ts
│ │ - git diff HEAD path/to/test.ts
│ │ - git log -p path/to/file.ts
│ │
│ ├─► Compare old vs new behavior
│ │
│ └─► ⚠️ Likely unintended side effect!
│ └─► Fix code to satisfy BOTH old AND new tests
│ - Refine implementation
│ - Add conditional logic if needed
│ - Ensure backward compatibility
│ - Keep existing functionality intact
✅ Git commands are EXPLICITLY ALLOWED for analysis:
# View old version of a file
git show HEAD:src/server/modules/user/user.service.ts
# See what changed in a file
git diff HEAD src/server/modules/user/user.service.ts
# View file from specific commit
git show abc123:path/to/file.ts
# See commit history for a file
git log -p --follow path/to/file.ts
# Compare branches
git diff main..HEAD path/to/file.ts
These commands help you understand:
// Scenario: You added a required field to User model
// Old test expects: { email, firstName }
// New behavior requires: { email, firstName, lastName }
// ✅ CORRECT: Update the test
it('should create user', async () => {
const user = await userService.create({
email: 'test@example.com',
firstName: 'John',
lastName: 'Doe', // ✅ Added required field
});
// ...
});
// Scenario: You changed authentication logic for new feature
// Old tests for different feature now fail unexpectedly
// ❌ WRONG: Just update the failing tests
// ✅ CORRECT: Investigate and fix the code
// 1. Use git to see old implementation
// git show HEAD:src/server/modules/auth/auth.service.ts
// 2. Identify the unintended side effect
// 3. Refine your code to avoid breaking existing functionality
// Example fix: Add conditional logic
async authenticate(user: User, options?: AuthOptions) {
// Your new feature logic
if (options?.useNewBehavior) {
return this.newAuthMethod(user);
}
// Preserve existing behavior for backward compatibility
return this.existingAuthMethod(user);
}
✅ DO update existing tests when:
❌ DON'T update existing tests when:
🔍 INVESTIGATE when:
Run ALL tests (not just story tests)
npm test
If existing tests fail:
# Identify which tests failed
# For each failing test, decide:
For intentional changes:
For unclear failures:
git show to see old codegit diff to see your changesValidate:
# All tests (old + new) should pass
npm test
🚩 Warning signs of unintended side effects:
main branch now failWhen you see red flags:
🚨 NEVER create git commits unless explicitly requested by the developer.
This is a NON-NEGOTIABLE RULE:
git add, git commit, or git push unless explicitly asked✅ ONLY create git commits when:
Why this is important:
Your responsibility:
In your final report, you may remind the developer:
## Next Steps
The implementation is complete and all tests are passing.
You may want to review and commit these changes when ready.
But NEVER execute git commands yourself unless explicitly requested.
@Restricted() decorators@Roles() or @UnifiedField({roles}) to more permissive rolessecurityCheck() logic to bypass securityCORRECT approach:
// Create test user (every logged-in user has the Role.S_USER role)
const res = await testHelper.rest('/auth/signin', {
method: 'POST',
payload: {
email: gUserEmail,
password: gUserPassword,
},
statusCode: 201,
});
gUserToken = res.token;
// Verify user
await db.collection('users').updateOne({ _id: new ObjectId(res.id) }, { $set: { verified: true } });
// Or optionally specify additional roles (e.g., admin, if really necessary)
await db.collection('users').findOneAndUpdate({ _id: new ObjectId(res.id) }, { $set: { roles: ['admin'], verified: true } });
// Test with authenticated user via token
const result = testHelper.rest('/api/products', {
method: 'POST',
payload: input,
statusCode: 201,
token: gUserToken,
});
WRONG approach (NEVER do this):
// ❌ DON'T remove @Restricted decorator from controller
// ❌ DON'T change @Roles(ADMIN) to @Roles(S_USER)
// ❌ DON'T disable authentication
declare KEYWORD FOR PROPERTIES⚠️ IMPORTANT RULE: DO NOT use the declare keyword when defining properties in classes!
The declare keyword in TypeScript signals that a property is only a type declaration without a runtime value. This prevents decorators from being properly applied and overridden.
❌ WRONG - Using declare:
export class ProductCreateInput extends ProductInput {
declare name: string; // ❌ WRONG - Decorator won't be applied!
declare price: number; // ❌ WRONG - Decorator won't be applied!
}
✅ CORRECT - Without declare:
export class ProductCreateInput extends ProductInput {
@UnifiedField({ description: 'Product name' })
name: string; // ✅ CORRECT - Decorator works properly
@UnifiedField({ description: 'Product price' })
price: number; // ✅ CORRECT - Decorator works properly
}
Why this matters:
@UnifiedField(), @Restricted(), and other decorators need actual property declarations to attach metadatadeclare prevents decorators from being properly overriddendeclare properties don't exist at runtime, breaking the decorator systemCorrect approach:
Use the override keyword (when appropriate) but NEVER declare:
export class ProductCreateInput extends ProductInput {
// ✅ Use override when useDefineForClassFields is enabled
override name: string;
// ✅ Apply decorators directly - they will override parent decorators
@UnifiedField({ description: 'Product name', isOptional: false })
override price: number;
}
Remember: declare = no decorators = broken functionality!
You should work autonomously as much as possible:
Only ask developer when:
When all tests pass, provide a comprehensive report:
# Story Implementation Complete ✅
## Story: [Story Name]
### Tests Created
- Location: test/stories/[filename].story.test.ts
- Test cases: [number] scenarios
- Coverage: [coverage percentage if available]
### Implementation Summary
- Modules created/modified: [list]
- Objects created/modified: [list]
- Properties added: [list]
- Other changes: [list]
### Test Results
✅ All [number] tests passing
- [Brief summary of test scenarios]
### Code Quality
- Followed existing patterns: ✅
- Security preserved: ✅
- No new dependencies added: ✅ (or list new dependencies with justification)
- Code duplication checked: ✅
- Refactoring performed: [Yes/No - describe if yes]
- Database indexes added: ✅
### Security Review
- Authentication/Authorization: ✅ All decorators intact
- Input validation: ✅ All inputs validated
- Data exposure: ✅ Sensitive fields hidden
- Ownership checks: ✅ Proper authorization in services
- Injection prevention: ✅ No SQL/NoSQL injection risks
- Error handling: ✅ No data leakage in errors
- Security tests: ✅ All authorization tests pass
### Refactoring (if performed)
- Extracted helper functions: [list with brief description]
- Consolidated code paths: [describe]
- Removed duplication: [describe]
- Tests still passing after refactoring: ✅
### Files Modified
1. [file path] - [what changed]
2. [file path] - [what changed]
...
### Next Steps (if any)
- [Any recommendations or follow-up items]
// Study existing tests to see the exact pattern used
// Common pattern example:
// Create test user (every logged-in user has the Role.S_USER role)
const resUser = await testHelper.rest('/auth/signin', {
method: 'POST',
payload: {
email: gUserEmail,
password: gUserPassword,
},
statusCode: 201,
});
gUserToken = resUser.token;
await db.collection('users').updateOne({ _id: new ObjectId(resUser.id) }, { $set: { verified: true } });
// Create admin user
const resAdmin = await testHelper.rest('/auth/signin', {
method: 'POST',
payload: {
email: gAdminEmail,
password: gAdminPassword,
},
statusCode: 201,
});
gAdminToken = resAdmin.token;
await db.collection('users').updateOne({ _id: new ObjectId(resAdmin.id) }, { $set: { roles: ['admin'], verified: true } });
// Study existing tests for the exact pattern
// Common REST API pattern:
const response = await testHelper.rest('/api/products', {
method: 'POST',
payload: input,
statusCode: 201,
token: gUserToken,
});
// Common GraphQL pattern:
const result = await testHelper.graphQl(
{
arguments: {
field: value,
},
fields: ['id', 'name', { user: ['id', 'email'] }],
name: 'findProducts',
type: TestGraphQLType.QUERY,
},
{ token: gUserToken },
);
describe('Feature Story', () => {
// Shared setup
let app: INestApplication;
let adminUser: User;
let normalUser: User;
beforeAll(async () => {
// Initialize app, database, users
});
afterAll(async () => {
// Cleanup
});
describe('Happy Path', () => {
it('should work for authorized user', async () => {
// Test
});
});
describe('Error Cases', () => {
it('should reject unauthorized access', async () => {
// Test
});
it('should validate input data', async () => {
// Test
});
});
describe('Edge Cases', () => {
it('should handle special scenarios', async () => {
// Test
});
});
});
When to invoke nest-server-generator skill:
During Step 4 (Implementation), you should use the nest-server-generator skill for:
Module creation:
lt server module ModuleName --no-interactive [options]
Object creation:
lt server object ObjectName [options]
Adding properties:
lt server addProp ModuleName propertyName:type [options]
Understanding existing code:
Best Practice: Invoke the skill explicitly when you need to create or modify NestJS components, rather than editing files manually.
Your goal is to deliver fully tested, high-quality, maintainable, and secure features that integrate seamlessly with the existing codebase while maintaining all security standards.