Major updates: - Added 35+ new skills from awesome-opencode-skills and antigravity repos - Merged SEO skills into seo-master - Merged architecture skills into architecture - Merged security skills into security-auditor and security-coder - Merged testing skills into testing-master and testing-patterns - Merged pentesting skills into pentesting - Renamed website-creator to thai-frontend-dev - Replaced skill-creator with github version - Removed Chutes references (use MiniMax API instead) - Added install-openclaw-skills.sh for cross-platform installation - Updated .env.example with MiniMax API credentials
405 lines
7.6 KiB
Markdown
405 lines
7.6 KiB
Markdown
---
|
|
name: testing-patterns
|
|
description: |
|
|
Testing patterns for JavaScript/TypeScript and Python.
|
|
pytest, Jest, mocking, fixtures, TDD patterns.
|
|
Use when writing tests or setting up test infrastructure.
|
|
---
|
|
|
|
# Testing Patterns
|
|
|
|
Comprehensive testing patterns for JavaScript/TypeScript and Python.
|
|
|
|
---
|
|
|
|
## Quick Reference
|
|
|
|
| Language | Framework | Use Section |
|
|
|----------|-----------|-------------|
|
|
| JavaScript/TypeScript | Jest, Vitest | **JavaScript Testing** |
|
|
| Python | pytest | **Python Testing** |
|
|
| Shell | bats | **Shell Testing** |
|
|
|
|
---
|
|
|
|
## JavaScript/TypeScript Testing
|
|
|
|
### Frameworks
|
|
- **Jest** - Most popular, built-in mocking
|
|
- **Vitest** - Vite-native, fast, compatible with Jest
|
|
- **Mocha** - Flexible, requires external assertions
|
|
|
|
### Jest Setup
|
|
```javascript
|
|
// jest.config.js
|
|
module.exports = {
|
|
testEnvironment: 'node',
|
|
testMatch: ['**/__tests__/**/*.test.js'],
|
|
collectCoverage: true,
|
|
coveragePathIgnorePatterns: ['/node_modules/'],
|
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
|
};
|
|
```
|
|
|
|
### Basic Test Structure
|
|
```javascript
|
|
describe('Calculator', () => {
|
|
let calculator;
|
|
|
|
beforeEach(() => {
|
|
calculator = new Calculator();
|
|
});
|
|
|
|
it('should add two numbers', () => {
|
|
expect(calculator.add(2, 3)).toBe(5);
|
|
});
|
|
|
|
it('should throw for invalid input', () => {
|
|
expect(() => calculator.add('a', 3)).toThrow(TypeError);
|
|
});
|
|
});
|
|
```
|
|
|
|
### Mocking
|
|
```javascript
|
|
// Mock a function
|
|
const mockFn = jest.fn();
|
|
mockFn.mockReturnValue(42);
|
|
|
|
// Mock a module
|
|
jest.mock('./api', () => ({
|
|
fetchUser: jest.fn().mockResolvedValue({ id: 1, name: 'John' }),
|
|
}));
|
|
|
|
// Spy on a method
|
|
const spy = jest.spyOn(calculator, 'add');
|
|
calculator.add(2, 3);
|
|
expect(spy).toHaveBeenCalledWith(2, 3);
|
|
```
|
|
|
|
### Async Testing
|
|
```javascript
|
|
it('should fetch user', async () => {
|
|
const user = await fetchUser(1);
|
|
expect(user.name).toBe('John');
|
|
});
|
|
|
|
it('should handle error', async () => {
|
|
await expect(fetchUser(-1)).rejects.toThrow('Not found');
|
|
});
|
|
```
|
|
|
|
### React Component Testing
|
|
```javascript
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
|
|
test('should render login form', () => {
|
|
render(<LoginForm />);
|
|
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
fireEvent.change(screen.getByLabelText('Email'), {
|
|
target: { value: 'test@example.com' },
|
|
});
|
|
});
|
|
```
|
|
|
|
### Vitest (Modern Alternative)
|
|
```javascript
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { mount } from '@vue/test-utils';
|
|
|
|
describe('Counter', () => {
|
|
it('increments', async () => {
|
|
const wrapper = mount(Counter);
|
|
await wrapper.find('button').trigger('click');
|
|
expect(wrapper.text()).toContain('1');
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Python Testing (pytest)
|
|
|
|
### pytest Setup
|
|
```bash
|
|
pip install pytest pytest-cov pytest-mock pytest-asyncio
|
|
```
|
|
|
|
### Basic Test Structure
|
|
```python
|
|
import pytest
|
|
|
|
class TestCalculator:
|
|
@pytest.fixture
|
|
def calculator(self):
|
|
return Calculator()
|
|
|
|
def test_add(self, calculator):
|
|
assert calculator.add(2, 3) == 5
|
|
|
|
def test_invalid_input(self, calculator):
|
|
with pytest.raises(TypeError):
|
|
calculator.add("a", 3)
|
|
```
|
|
|
|
### Fixtures
|
|
```python
|
|
@pytest.fixture
|
|
def user():
|
|
return User(name="John", email="john@example.com")
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
session = create_session()
|
|
yield session
|
|
session.close()
|
|
|
|
def test_user(db_session, user):
|
|
db_session.add(user)
|
|
db_session.commit()
|
|
assert db_session.query(User).count() == 1
|
|
```
|
|
|
|
### Mocking
|
|
```python
|
|
from unittest.mock import Mock, patch
|
|
|
|
@patch('api.fetch_user')
|
|
def test_fetch_user(mock_fetch):
|
|
mock_fetch.return_value = {"id": 1, "name": "John"}
|
|
|
|
result = get_user(1)
|
|
assert result["name"] == "John"
|
|
mock_fetch.assert_called_once_with(1)
|
|
|
|
# Async mocking
|
|
@pytest.mark.asyncio
|
|
@patch('api.async_fetch_user')
|
|
async def test_async_fetch(mock_fetch):
|
|
mock_fetch.return_value = {"id": 1}
|
|
result = await get_user(1)
|
|
assert result["id"] == 1
|
|
```
|
|
|
|
### Parameterized Tests
|
|
```python
|
|
@pytest.mark.parametrize("input,expected", [
|
|
(2, 4),
|
|
(3, 9),
|
|
(4, 16),
|
|
])
|
|
def test_square(input, expected):
|
|
assert square(input) == expected
|
|
```
|
|
|
|
### Testing Exceptions
|
|
```python
|
|
def test_raises():
|
|
with pytest.raises(ValueError, match="must be positive"):
|
|
factorial(-1)
|
|
```
|
|
|
|
### Database Testing
|
|
```python
|
|
@pytest.fixture
|
|
def test_db():
|
|
engine = create_test_engine()
|
|
Base.metadata.create_all(engine)
|
|
yield session
|
|
Base.metadata.drop_all(engine)
|
|
|
|
def test_user_crud(test_db):
|
|
user = User(name="John")
|
|
test_db.add(user)
|
|
test_db.commit()
|
|
|
|
retrieved = test_db.query(User).first()
|
|
assert retrieved.name == "John"
|
|
```
|
|
|
|
### Async Testing
|
|
```python
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
async def test_async_fetch():
|
|
result = await fetch_data()
|
|
assert result is not None
|
|
```
|
|
|
|
---
|
|
|
|
## Shell Testing (BATS)
|
|
|
|
### BATS Setup
|
|
```bash
|
|
bats my_tests.bats
|
|
```
|
|
|
|
### Basic Test Structure
|
|
```bash
|
|
#!/usr/bin/env bats
|
|
|
|
@test "addition" {
|
|
result=$(echo "2 + 3" | bc)
|
|
[ "$result" -eq 5 ]
|
|
}
|
|
|
|
@test "file exists" {
|
|
touch /tmp/testfile
|
|
[ -f /tmp/testfile ]
|
|
}
|
|
|
|
@test "command succeeds" {
|
|
run ls /tmp
|
|
[ "$status" -eq 0 ]
|
|
}
|
|
```
|
|
|
|
### Setup/Teardown
|
|
```bash
|
|
setup() {
|
|
echo "Before each test"
|
|
}
|
|
|
|
teardown() {
|
|
echo "After each test"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Best Practices
|
|
|
|
### Test Naming
|
|
```javascript
|
|
// Good
|
|
describe('UserService', () => {
|
|
it('should return null for non-existent user');
|
|
it('should hash password before storing');
|
|
it('should throw ValidationError for invalid email');
|
|
});
|
|
|
|
// Bad
|
|
it('test1');
|
|
it('user test');
|
|
```
|
|
|
|
### AAA Pattern
|
|
```javascript
|
|
// Arrange - Set up test data
|
|
const user = { name: 'John', email: 'john@example.com' };
|
|
|
|
// Act - Execute the code
|
|
const result = userService.create(user);
|
|
|
|
// Assert - Verify the outcome
|
|
expect(result.id).toBeDefined();
|
|
expect(result.password).not.toBe('plaintext');
|
|
```
|
|
|
|
### Test Isolation
|
|
```javascript
|
|
// Bad - shared state
|
|
let user;
|
|
it('creates', () => { user = create(); });
|
|
it('modifies', () => { modify(user.id); }); // depends on first test
|
|
|
|
// Good - isolated
|
|
it('creates and modifies', () => {
|
|
const user = create();
|
|
modify(user.id);
|
|
});
|
|
```
|
|
|
|
### Meaningful Assertions
|
|
```javascript
|
|
// Bad
|
|
expect(result).toBeDefined();
|
|
|
|
// Good
|
|
expect(result).toEqual({
|
|
id: '123',
|
|
name: 'John',
|
|
email: 'john@example.com'
|
|
});
|
|
```
|
|
|
|
### Fast Tests
|
|
- Mock external APIs
|
|
- Use in-memory databases
|
|
- Parallel test execution
|
|
- Avoid `sleep()` - use explicit waits
|
|
|
|
---
|
|
|
|
## Mocking Patterns
|
|
|
|
### HTTP Requests (JavaScript)
|
|
```javascript
|
|
// Mock fetch
|
|
global.fetch = jest.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({ data: 'test' }),
|
|
});
|
|
|
|
// Mock axios
|
|
jest.mock('axios');
|
|
axios.get.mockResolvedValue({ data: { id: 1 } });
|
|
```
|
|
|
|
### HTTP Requests (Python)
|
|
```python
|
|
import requests_mock
|
|
from requests_mock import DELETE, GET, POST
|
|
|
|
def test_api(requests_mock):
|
|
requests_mock.register_uri(GET, 'https://api.example.com/user',
|
|
json={'id': 1, 'name': 'John'})
|
|
|
|
response = requests.get('https://api.example.com/user')
|
|
assert response.json()['name'] == 'John'
|
|
```
|
|
|
|
### Time/Mocking
|
|
```javascript
|
|
// Jest
|
|
jest.useFakeTimers();
|
|
jest.setSystemTime(new Date('2024-01-01'));
|
|
// ... test code
|
|
jest.useRealTimers();
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
from freezegun import freeze_time
|
|
|
|
@freeze_time("2024-01-01")
|
|
def test_time_based():
|
|
assert get_current_year() == 2024
|
|
```
|
|
|
|
---
|
|
|
|
## Coverage
|
|
|
|
### Jest Coverage
|
|
```javascript
|
|
// jest.config.js
|
|
module.exports = {
|
|
collectCoverage: true,
|
|
coverageThreshold: {
|
|
global: {
|
|
branches: 70,
|
|
functions: 70,
|
|
lines: 70,
|
|
statements: 70,
|
|
},
|
|
},
|
|
};
|
|
```
|
|
|
|
### pytest Coverage
|
|
```bash
|
|
pytest --cov=src --cov-report=html --cov-fail-under=70
|
|
```
|