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:
412
.kilo/skills/nodejs-testing-jest/SKILL.md
Normal file
412
.kilo/skills/nodejs-testing-jest/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user