feat: Import 35+ skills, merge duplicates, add openclaw installer
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
This commit is contained in:
404
skills/testing-patterns/SKILL.md
Normal file
404
skills/testing-patterns/SKILL.md
Normal file
@@ -0,0 +1,404 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user