feat: add comprehensive NodeJS development skills and rules

Based on Planner and Memory Manager analysis:

New Skills (8):
- nodejs-express-patterns: App structure, routing, middleware
- nodejs-security-owasp: OWASP Top 10 security practices
- nodejs-testing-jest: Unit/integration tests, mocking
- nodejs-auth-jwt: JWT authentication, OAuth, sessions
- nodejs-error-handling: Error classes, middleware, async handlers
- nodejs-middleware-patterns: Auth, validation, rate limiting
- nodejs-db-patterns: SQLite, PostgreSQL, MongoDB patterns
- nodejs-npm-management: package.json, scripts, dependencies

New Rules:
- nodejs.md: Code style, security, best practices

Updated:
- backend-developer.md: Added skills reference table

Milestone: #48 NodeJS Development Coverage
Related: Planner & Memory Manager analysis results
This commit is contained in:
¨NW¨
2026-04-05 02:39:06 +01:00
parent fbc1f6122f
commit 8fcd8f8a9b
10 changed files with 3318 additions and 1 deletions

View File

@@ -0,0 +1,412 @@
# NodeJS Testing with Jest
Comprehensive testing patterns for Node.js applications using Jest.
## Setup
```bash
npm install --save-dev jest @types/jest ts-jest
```
```json
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
```
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
testMatch: ['**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.d.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
```
## Test Structure
```
tests/
├── unit/
│ ├── services/
│ │ └── userService.test.js
│ ├── controllers/
│ │ └── userController.test.js
│ └── utils/
│ └── helpers.test.js
├── integration/
│ ├── api/
│ │ └── users.test.js
│ └── db/
│ └── connection.test.js
└── mocks/
└── mockData.js
```
## Unit Tests
### Service Tests
```javascript
// tests/unit/services/userService.test.js
const userService = require('../../../src/services/userService');
const User = require('../../../src/models/User');
const { hashPassword } = require('../../../src/utils/password');
// Mock dependencies
jest.mock('../../../src/models/User');
jest.mock('../../../src/utils/password');
describe('UserService', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should create a new user with hashed password', async () => {
// Arrange
const userData = {
email: 'test@example.com',
password: 'password123',
name: 'Test User'
};
const hashedPassword = 'hashed_password';
const createdUser = { id: 1, ...userData, password: hashedPassword };
User.findByEmail.mockResolvedValue(null);
hashPassword.mockResolvedValue(hashedPassword);
User.create.mockResolvedValue(createdUser);
// Act
const result = await userService.create(userData);
// Assert
expect(User.findByEmail).toHaveBeenCalledWith(userData.email);
expect(hashPassword).toHaveBeenCalledWith(userData.password);
expect(User.create).toHaveBeenCalledWith({
...userData,
password: hashedPassword
});
expect(result).toEqual(createdUser);
});
it('should throw error if email already exists', async () => {
// Arrange
const userData = { email: 'existing@example.com', password: 'pass' };
User.findByEmail.mockResolvedValue({ id: 1 });
// Act & Assert
await expect(userService.create(userData))
.rejects.toThrow('Email already registered');
});
});
});
```
### Controller Tests
```javascript
// tests/unit/controllers/userController.test.js
const userController = require('../../../src/controllers/userController');
const userService = require('../../../src/services/userService');
jest.mock('../../../src/services/userService');
describe('UserController', () => {
let req, res, next;
beforeEach(() => {
req = { body: {}, params: {}, user: {} };
res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
next = jest.fn();
jest.clearAllMocks();
});
describe('register', () => {
it('should register user and return 201', async () => {
// Arrange
req.body = { email: 'test@example.com', password: 'password123' };
const user = { id: 1, email: 'test@example.com' };
userService.create.mockResolvedValue(user);
// Act
await userController.register(req, res, next);
// Assert
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith({
status: 'success',
data: { user }
});
expect(next).not.toHaveBeenCalled();
});
it('should call next with error on failure', async () => {
// Arrange
const error = new Error('Database error');
userService.create.mockRejectedValue(error);
// Act
await userController.register(req, res, next);
// Assert
expect(next).toHaveBeenCalledWith(error);
});
});
});
```
## Integration Tests
### API Tests
```javascript
// tests/integration/api/users.test.js
const request = require('supertest');
const app = require('../../../src/app');
const User = require('../../../src/models/User');
// Setup/Teardown
beforeAll(async () => {
await connectTestDatabase();
});
afterAll(async () => {
await disconnectTestDatabase();
});
beforeEach(async () => {
await User.deleteMany({});
});
describe('Users API', () => {
describe('POST /api/users/register', () => {
it('should register a new user', async () => {
const response = await request(app)
.post('/api/users/register')
.send({
email: 'test@example.com',
password: 'password123',
name: 'Test User'
})
.expect(201);
expect(response.body.status).toBe('success');
expect(response.body.data.user).toHaveProperty('id');
expect(response.body.data.user.email).toBe('test@example.com');
});
it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users/register')
.send({
email: 'invalid-email',
password: 'password123'
})
.expect(400);
expect(response.body.status).toBe('fail');
});
});
describe('GET /api/users/me', () => {
it('should return user profile when authenticated', async () => {
// Create user and get token
const user = await User.create({
email: 'test@example.com',
password: await hashPassword('password123')
});
const token = generateToken({ id: user.id });
const response = await request(app)
.get('/api/users/me')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.data.user.email).toBe('test@example.com');
});
it('should return 401 when not authenticated', async () => {
await request(app)
.get('/api/users/me')
.expect(401);
});
});
});
```
## Mocking
### Mock Modules
```javascript
// Mock external module
jest.mock('axios');
// Mock internal module
jest.mock('../../../src/services/emailService', () => ({
sendEmail: jest.fn()
}));
// Mock environment variable
process.env.JWT_SECRET = 'test-secret';
```
### Mock Functions
```javascript
// Create mock function
const mockFn = jest.fn();
// Set return value
mockFn.mockReturnValue('value');
mockFn.mockReturnValueOnce('value once');
// Set implementation
mockFn.mockImplementation((x) => x * 2);
// Set resolved value (async)
mockFn.mockResolvedValue({ data: 'value' });
mockFn.mockRejectedValue(new Error('failed'));
// Check calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockFn).toHaveReturnedWith('value');
```
## Test Coverage
```javascript
// Run coverage
npm run test:coverage
// Coverage report
// tests/unit/services/userService.test.js
// File | % Stmts | % Branch | % Funcs | % Lines |
//--------------|---------|----------|---------|---------|
// userService | 95.45 | 87.50 | 100.00 | 95.45 |
```
## Best Practices
### Don't Do This
```javascript
// ❌ Test implementation details
it('should set isLoading to true', () => {
component.setState({ isLoading: true });
expect(component.state().isLoading).toBe(true);
});
// ❌ Test everything in one test
it('should work', async () => {
await request(app).post('/users').send(data);
await request(app).get('/users/1');
// ...many more assertions
});
// ❌ Use real database in unit tests
const db = require('mongoose').connect('mongodb://localhost/test');
```
### Do This
```javascript
// ✅ Test behavior
it('should show loading indicator', () => {
render(<Component />);
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
// ✅ Test one thing per test
it('should create user', async () => {
await request(app).post('/users').send(data).expect(201);
});
it('should return created user', async () => {
const res = await request(app).post('/users').send(data);
expect(res.body.data.user.email).toBe(data.email);
});
// ✅ Use mock database
jest.mock('../../../src/db/connection');
```
## Testing Async Code
```javascript
// Promises
it('should resolve with data', async () => {
const result = await service.getData();
expect(result).toEqual(expectedData);
});
// Callbacks
it('should call callback', (done) => {
service.asyncOperation((result) => {
expect(result).toBe('expected');
done();
});
});
// Timers
jest.useFakeTimers();
it('should timeout after 5s', () => {
const callback = jest.fn();
setTimeout(callback, 5000);
jest.advanceTimersByTime(5000);
expect(callback).toHaveBeenCalled();
});
```
## Test Fixtures
```javascript
// tests/fixtures/users.js
module.exports = {
validUser: {
email: 'test@example.com',
password: 'Password123!',
name: 'Test User'
},
invalidUser: {
email: 'invalid',
password: 'short'
},
adminUser: {
email: 'admin@example.com',
password: 'Admin123!',
name: 'Admin User',
role: 'admin'
}
};
// Usage
const { validUser, invalidUser } = require('../fixtures/users');
```
## See Also
- `nodejs-express-patterns` - Express patterns
- `nodejs-auth-jwt` - Authentication testing
- `nodejs-error-handling` - Error scenarios