backend-testing
npx skills add https://github.com/supercent-io/skills-template --skill backend-testing
Agent 安装分布
Skill 文档
Backend Testing
When to use this skill
ì´ ì¤í¬ì í¸ë¦¬ê±°í´ì¼ íë 구체ì ì¸ ìí©ì ëì´í©ëë¤:
- ì ê¸°ë¥ ê°ë°: TDD(Test-Driven Development) ë°©ìì¼ë¡ í ì¤í¸ 먼ì ìì±
- API ìëí¬ì¸í¸ ì¶ê°: REST APIì ì±ê³µ/ì¤í¨ ì¼ì´ì¤ í ì¤í¸
- ë²ê·¸ ìì : íê· ë°©ì§ë¥¼ ìí í ì¤í¸ ì¶ê°
- 리í©í ë§ ì : 기존 ëìì ë³´ì¥íë í ì¤í¸ ìì±
- CI/CD ì¤ì : ìëíë í ì¤í¸ íì´íë¼ì¸ 구ì¶
ì ë ¥ íì (Input Format)
ì¬ì©ìë¡ë¶í° ë°ìì¼ í ì ë ¥ì íìê³¼ íì/ì í ì ë³´:
íì ì ë³´
- íë ììí¬: Express, Django, FastAPI, Spring Boot ë±
- í ì¤í¸ ë구: Jest, Pytest, Mocha/Chai, JUnit ë±
- í ì¤í¸ ëì: API ìëí¬ì¸í¸, ë¹ì¦ëì¤ ë¡ì§, DB ìì ë±
ì í ì ë³´
- ë°ì´í°ë² ì´ì¤: PostgreSQL, MySQL, MongoDB (기본ê°: in-memory DB)
- ëª¨í¹ ë¼ì´ë¸ë¬ë¦¬: jest.mock, sinon, unittest.mock (기본ê°: íë ììí¬ ë´ì¥)
- 커ë²ë¦¬ì§ 목í: 80%, 90% ë± (기본ê°: 80%)
- E2E ë구: Supertest, TestClient, RestAssured (ì í)
ì ë ¥ ìì
Express.js APIì ì¬ì©ì ì¸ì¦ ìëí¬ì¸í¸ë¥¼ í
ì¤í¸í´ì¤:
- íë ììí¬: Express + TypeScript
- í
ì¤í¸ ë구: Jest + Supertest
- ëì: POST /auth/register, POST /auth/login
- DB: PostgreSQL (í
ì¤í¸ì© in-memory)
- 커ë²ë¦¬ì§: 90% ì´ì
Instructions
ë¨ê³ë³ë¡ ì ííê² ë°ë¼ì¼ í ìì ìì를 ëª ìí©ëë¤.
Step 1: í ì¤í¸ íê²½ ì¤ì
í ì¤í¸ íë ììí¬ ë° ë구를 ì¤ì¹íê³ ì¤ì í©ëë¤.
ìì ë´ì©:
- í ì¤í¸ ë¼ì´ë¸ë¬ë¦¬ ì¤ì¹
- í ì¤í¸ ë°ì´í°ë² ì´ì¤ ì¤ì (in-memory ëë ë³ë DB)
- íê²½ë³ì ë¶ë¦¬ (.env.test)
- jest.config.js ëë pytest.ini ì¤ì
ìì (Node.js + Jest + Supertest):
npm install --save-dev jest ts-jest @types/jest supertest @types/supertest
jest.config.js:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/__tests__/**'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts']
};
setup.ts (í ì¤í¸ ì ì ì¤ì ):
import { db } from '../database';
// ê° í
ì¤í¸ ì DB ì´ê¸°í
beforeEach(async () => {
await db.migrate.latest();
await db.seed.run();
});
// ê° í
ì¤í¸ í ì 리
afterEach(async () => {
await db.migrate.rollback();
});
// 모ë í
ì¤í¸ ìë£ í ì°ê²° ì¢
ë£
afterAll(async () => {
await db.destroy();
});
Step 2: Unit Test ìì± (ë¹ì¦ëì¤ ë¡ì§)
ê°ë³ í¨ì/í´ëì¤ì ë¨ì í ì¤í¸ë¥¼ ìì±í©ëë¤.
ìì ë´ì©:
- ìì í¨ì í ì¤í¸ (ìì¡´ì± ìì)
- 모í¹ì íµí ìì¡´ì± ê²©ë¦¬
- Edge case í ì¤í¸ (ê²½ê³ê°, ìì¸)
- AAA í¨í´ (Arrange-Act-Assert)
íë¨ ê¸°ì¤:
- ì¸ë¶ ìì¡´ì±(DB, API) ìì â ìì Unit Test
- ì¸ë¶ ìì¡´ì± ìì â Mock/Stub ì¬ì©
- ë³µì¡í ë¡ì§ â ë¤ìí ì ë ¥ ì¼ì´ì¤ í ì¤í¸
ìì (ë¹ë°ë²í¸ ê²ì¦ í¨ì):
// src/utils/password.ts
export function validatePassword(password: string): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!/[A-Z]/.test(password)) {
errors.push('Password must contain uppercase letter');
}
if (!/[a-z]/.test(password)) {
errors.push('Password must contain lowercase letter');
}
if (!/\d/.test(password)) {
errors.push('Password must contain number');
}
if (!/[!@#$%^&*]/.test(password)) {
errors.push('Password must contain special character');
}
return { valid: errors.length === 0, errors };
}
// src/__tests__/utils/password.test.ts
import { validatePassword } from '../../utils/password';
describe('validatePassword', () => {
it('should accept valid password', () => {
const result = validatePassword('Password123!');
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should reject password shorter than 8 characters', () => {
const result = validatePassword('Pass1!');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Password must be at least 8 characters');
});
it('should reject password without uppercase', () => {
const result = validatePassword('password123!');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Password must contain uppercase letter');
});
it('should reject password without lowercase', () => {
const result = validatePassword('PASSWORD123!');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Password must contain lowercase letter');
});
it('should reject password without number', () => {
const result = validatePassword('Password!');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Password must contain number');
});
it('should reject password without special character', () => {
const result = validatePassword('Password123');
expect(result.valid).toBe(false);
expect(result.errors).toContain('Password must contain special character');
});
it('should return multiple errors for invalid password', () => {
const result = validatePassword('pass');
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(1);
});
});
Step 3: Integration Test (API ìëí¬ì¸í¸)
API ìëí¬ì¸í¸ì íµí© í ì¤í¸ë¥¼ ìì±í©ëë¤.
ìì ë´ì©:
- HTTP ìì²/ìëµ í ì¤í¸
- ì±ê³µ ì¼ì´ì¤ (200, 201)
- ì¤í¨ ì¼ì´ì¤ (400, 401, 404, 500)
- ì¸ì¦/ê¶í í ì¤í¸
- ì ë ¥ ê²ì¦ í ì¤í¸
íì¸ ì¬í:
- Status code íì¸
- Response body 구조 ê²ì¦
- Database ìí ë³í íì¸
- ìë¬ ë©ìì§ ê²ì¦
ìì (Express.js + Supertest):
// src/__tests__/api/auth.test.ts
import request from 'supertest';
import app from '../../app';
import { db } from '../../database';
describe('POST /auth/register', () => {
it('should register new user successfully', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'testuser',
password: 'Password123!'
});
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('user');
expect(response.body).toHaveProperty('accessToken');
expect(response.body.user.email).toBe('test@example.com');
// DBì ì¤ì ë¡ ì ì¥ëìëì§ íì¸
const user = await db.user.findUnique({ where: { email: 'test@example.com' } });
expect(user).toBeTruthy();
expect(user.username).toBe('testuser');
});
it('should reject duplicate email', async () => {
// 첫 ë²ì§¸ ì¬ì©ì ìì±
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'user1',
password: 'Password123!'
});
// ê°ì ì´ë©ì¼ë¡ ë ë²ì§¸ ìë
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'user2',
password: 'Password123!'
});
expect(response.status).toBe(409);
expect(response.body.error).toContain('already exists');
});
it('should reject weak password', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'testuser',
password: 'weak'
});
expect(response.status).toBe(400);
expect(response.body.error).toBeDefined();
});
it('should reject missing fields', async () => {
const response = await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com'
// username, password ëë½
});
expect(response.status).toBe(400);
});
});
describe('POST /auth/login', () => {
beforeEach(async () => {
// í
ì¤í¸ì© ì¬ì©ì ìì±
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
username: 'testuser',
password: 'Password123!'
});
});
it('should login with valid credentials', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'Password123!'
});
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
expect(response.body.user.email).toBe('test@example.com');
});
it('should reject invalid password', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'WrongPassword123!'
});
expect(response.status).toBe(401);
expect(response.body.error).toContain('Invalid credentials');
});
it('should reject non-existent user', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'nonexistent@example.com',
password: 'Password123!'
});
expect(response.status).toBe(401);
});
});
Step 4: ì¸ì¦/ê¶í í ì¤í¸
JWT í í° ë° ê¶í ê¸°ë° ì ê·¼ ì ì´ë¥¼ í ì¤í¸í©ëë¤.
ìì ë´ì©:
- í í° ìì´ ì ê·¼ ì 401 íì¸
- ì í¨í í í°ì¼ë¡ ì ê·¼ ì±ê³µ íì¸
- ë§ë£ë í í° ì²ë¦¬ í ì¤í¸
- Role-based ê¶í í ì¤í¸
ìì:
describe('Protected Routes', () => {
let accessToken: string;
let adminToken: string;
beforeEach(async () => {
// ì¼ë° ì¬ì©ì í í°
const userResponse = await request(app)
.post('/api/auth/register')
.send({
email: 'user@example.com',
username: 'user',
password: 'Password123!'
});
accessToken = userResponse.body.accessToken;
// ê´ë¦¬ì í í°
const adminResponse = await request(app)
.post('/api/auth/register')
.send({
email: 'admin@example.com',
username: 'admin',
password: 'Password123!'
});
// DBìì roleì 'admin'ì¼ë¡ ë³ê²½
await db.user.update({
where: { email: 'admin@example.com' },
data: { role: 'admin' }
});
// ë¤ì ë¡ê·¸ì¸í´ì ì í í° ë°ê¸°
const loginResponse = await request(app)
.post('/api/auth/login')
.send({
email: 'admin@example.com',
password: 'Password123!'
});
adminToken = loginResponse.body.accessToken;
});
describe('GET /api/auth/me', () => {
it('should return current user with valid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', `Bearer ${accessToken}`);
expect(response.status).toBe(200);
expect(response.body.user.email).toBe('user@example.com');
});
it('should reject request without token', async () => {
const response = await request(app)
.get('/api/auth/me');
expect(response.status).toBe(401);
});
it('should reject request with invalid token', async () => {
const response = await request(app)
.get('/api/auth/me')
.set('Authorization', 'Bearer invalid-token');
expect(response.status).toBe(403);
});
});
describe('DELETE /api/users/:id (Admin only)', () => {
it('should allow admin to delete user', async () => {
const targetUser = await db.user.findUnique({ where: { email: 'user@example.com' } });
const response = await request(app)
.delete(`/api/users/${targetUser.id}`)
.set('Authorization', `Bearer ${adminToken}`);
expect(response.status).toBe(200);
});
it('should forbid non-admin from deleting user', async () => {
const targetUser = await db.user.findUnique({ where: { email: 'user@example.com' } });
const response = await request(app)
.delete(`/api/users/${targetUser.id}`)
.set('Authorization', `Bearer ${accessToken}`);
expect(response.status).toBe(403);
});
});
});
Step 5: Mocking ë° í ì¤í¸ 격리
ì¸ë¶ ìì¡´ì±ì 모í¹íì¬ í ì¤í¸ë¥¼ 격리í©ëë¤.
ìì ë´ì©:
- ì¸ë¶ API 모í¹
- ì´ë©ì¼ ë°ì¡ 모í¹
- íì¼ ìì¤í 모í¹
- ìê° ê´ë ¨ í¨ì 모í¹
ìì (ì¸ë¶ API 모í¹):
// src/services/emailService.ts
export async function sendVerificationEmail(email: string, token: string): Promise<void> {
const response = await fetch('https://api.sendgrid.com/v3/mail/send', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.SENDGRID_API_KEY}` },
body: JSON.stringify({
to: email,
subject: 'Verify your email',
html: `<a href="https://example.com/verify?token=${token}">Verify</a>`
})
});
if (!response.ok) {
throw new Error('Failed to send email');
}
}
// src/__tests__/services/emailService.test.ts
import { sendVerificationEmail } from '../../services/emailService';
// fetch 모í¹
global.fetch = jest.fn();
describe('sendVerificationEmail', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('should send email successfully', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
status: 200
});
await expect(sendVerificationEmail('test@example.com', 'token123'))
.resolves
.toBeUndefined();
expect(fetch).toHaveBeenCalledWith(
'https://api.sendgrid.com/v3/mail/send',
expect.objectContaining({
method: 'POST'
})
);
});
it('should throw error if email sending fails', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 500
});
await expect(sendVerificationEmail('test@example.com', 'token123'))
.rejects
.toThrow('Failed to send email');
});
});
Output format
ê²°ê³¼ë¬¼ì´ ë°ë¼ì¼ í ì íí íìì ì ìí©ëë¤.
기본 구조
íë¡ì í¸/
âââ src/
â âââ __tests__/
â â âââ setup.ts # í
ì¤í¸ ì ì ì¤ì
â â âââ utils/
â â â âââ password.test.ts # Unit tests
â â âââ services/
â â â âââ emailService.test.ts
â â âââ api/
â â âââ auth.test.ts # Integration tests
â â âââ users.test.ts
â âââ ...
âââ jest.config.js
âââ package.json
í ì¤í¸ ì¤í ì¤í¬ë¦½í¸ (package.json)
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2"
}
}
커ë²ë¦¬ì§ 리í¬í¸
$ npm run test:coverage
--------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------------|---------|----------|---------|---------|
All files | 92.5 | 88.3 | 95.2 | 92.8 |
auth/ | 95.0 | 90.0 | 100.0 | 95.0 |
middleware.ts | 95.0 | 90.0 | 100.0 | 95.0 |
routes.ts | 95.0 | 90.0 | 100.0 | 95.0 |
utils/ | 90.0 | 85.0 | 90.0 | 90.0 |
password.ts | 90.0 | 85.0 | 90.0 | 90.0 |
--------------------------|---------|----------|---------|---------|
Constraints
ë°ëì ì§ì¼ì¼ í ê·ì¹ê³¼ ê¸ì§ ì¬íì ëª ìí©ëë¤.
íì ê·ì¹ (MUST)
-
í ì¤í¸ 격리: ê° í ì¤í¸ë ë 립ì ì¼ë¡ ì¤í ê°ë¥í´ì¼ í¨
- beforeEach/afterEachë¡ ìí ì´ê¸°í
- í ì¤í¸ ììì ìì¡´íì§ ìì
-
ëª íí í ì¤í¸ëª : í ì¤í¸ê° 무ìì ê²ì¦íëì§ ì´ë¦ìì ì ì ìì´ì¼ í¨
- â ‘should reject duplicate email’
- â ‘test1’
-
AAA í¨í´: Arrange(ì¤ë¹) – Act(ì¤í) – Assert(ê²ì¦) 구조
- ê°ë ì± í¥ì
- í ì¤í¸ ìë ëª íí
ê¸ì§ ì¬í (MUST NOT)
-
íë¡ëì DB ì¬ì© ê¸ì§: í ì¤í¸ë ë³ë DB ëë in-memory DB ì¬ì©
- ì¤ì ë°ì´í° ìì¤ ìí
- í ì¤í¸ 격리 ë¶ê°
-
ì¤ì ì¸ë¶ API í¸ì¶ ê¸ì§: ì¸ë¶ ìë¹ì¤ë 모í¹
- ë¤í¸ìí¬ ìì¡´ì± ì ê±°
- í ì¤í¸ ìë í¥ì
- ë¹ì© ì ê°
-
Sleep/Timeout ë¨ì© ê¸ì§: ìê° ê¸°ë° í ì¤í¸ë ê°ì§ íì´ë¨¸ ì¬ì©
- jest.useFakeTimers()
- í ì¤í¸ ìë ì í ë°©ì§
ë³´ì ê·ì¹
- 민ê°ì ë³´ íëì½ë© ê¸ì§: í ì¤í¸ ì½ëìë API í¤, ë¹ë°ë²í¸ íëì½ë© ê¸ì§
- íê²½ë³ì ë¶ë¦¬: .env.test íì¼ ì¬ì©
Examples
ìì 1: Python FastAPI í ì¤í¸ (Pytest)
ìí©: FastAPI REST API í ì¤í¸
ì¬ì©ì ìì²:
FastAPIë¡ ë§ë ì¬ì©ì API를 pytestë¡ í
ì¤í¸í´ì¤.
ìµì¢ ê²°ê³¼:
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
# In-memory SQLite for tests
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
@pytest.fixture(scope="function")
def db_session():
Base.metadata.create_all(bind=engine)
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(db_session):
def override_get_db():
try:
yield db_session
finally:
db_session.close()
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
# tests/test_auth.py
def test_register_user_success(client):
response = client.post("/auth/register", json={
"email": "test@example.com",
"username": "testuser",
"password": "Password123!"
})
assert response.status_code == 201
assert "access_token" in response.json()
assert response.json()["user"]["email"] == "test@example.com"
def test_register_duplicate_email(client):
# First user
client.post("/auth/register", json={
"email": "test@example.com",
"username": "user1",
"password": "Password123!"
})
# Duplicate email
response = client.post("/auth/register", json={
"email": "test@example.com",
"username": "user2",
"password": "Password123!"
})
assert response.status_code == 409
assert "already exists" in response.json()["detail"]
def test_login_success(client):
# Register
client.post("/auth/register", json={
"email": "test@example.com",
"username": "testuser",
"password": "Password123!"
})
# Login
response = client.post("/auth/login", json={
"email": "test@example.com",
"password": "Password123!"
})
assert response.status_code == 200
assert "access_token" in response.json()
def test_protected_route_without_token(client):
response = client.get("/auth/me")
assert response.status_code == 401
def test_protected_route_with_token(client):
# Register and get token
register_response = client.post("/auth/register", json={
"email": "test@example.com",
"username": "testuser",
"password": "Password123!"
})
token = register_response.json()["access_token"]
# Access protected route
response = client.get("/auth/me", headers={
"Authorization": f"Bearer {token}"
})
assert response.status_code == 200
assert response.json()["email"] == "test@example.com"
Best practices
íì§ í¥ì
-
TDD (Test-Driven Development): ì½ë ìì± ì ì í ì¤í¸ 먼ì
- ì구ì¬í ëª íí
- ì¤ê³ ê°ì
- ëì 커ë²ë¦¬ì§ ìì°ì¤ë½ê² ë¬ì±
-
Given-When-Then í¨í´: BDD ì¤íì¼ë¡ í ì¤í¸ ìì±
it('should return 404 when user not found', async () => { // Given: ì¡´ì¬íì§ ìë ì¬ì©ì ID const nonExistentId = 'non-existent-uuid'; // When: í´ë¹ ì¬ì©ì ì¡°í ìë const response = await request(app).get(`/users/${nonExistentId}`); // Then: 404 ìëµ expect(response.status).toBe(404); }); -
Test Fixtures: ì¬ì¬ì© ê°ë¥í í ì¤í¸ ë°ì´í°
const validUser = { email: 'test@example.com', username: 'testuser', password: 'Password123!' };
í¨ì¨ì± ê°ì
- ë³ë ¬ ì¤í: Jestì
--maxWorkersìµì ì¼ë¡ í ì¤í¸ ìë í¥ì - Snapshot Testing: UI ì»´í¬ëí¸ë JSON ìëµ ì¤ë ì· ì ì¥
- Coverage ìê³ê°: jest.config.jsìì ìµì 커ë²ë¦¬ì§ ê°ì
ì주 ë°ìíë 문ì (Common Issues)
문ì 1: í ì¤í¸ ê° ìí ê³µì ë¡ ì¸í ì¤í¨
ì¦ì: ê°ë³ ì¤íì ì±ê³µíì§ë§ ì ì²´ ì¤í ì ì¤í¨
ìì¸: beforeEach/afterEach ëë½ì¼ë¡ DB ìí ê³µì
í´ê²°ë°©ë²:
beforeEach(async () => {
await db.migrate.rollback();
await db.migrate.latest();
});
문ì 2: “Jest did not exit one second after the test run”
ì¦ì: í ì¤í¸ ìë£ í íë¡ì¸ì¤ê° ì¢ ë£ëì§ ìì
ìì¸: DB ì°ê²°, ìë² ë±ì´ ì 리ëì§ ìì
í´ê²°ë°©ë²:
afterAll(async () => {
await db.destroy();
await server.close();
});
문ì 3: ë¹ë기 í ì¤í¸ íììì
ì¦ì: “Timeout – Async callback was not invoked”
ìì¸: async/await ëë½ ëë Promise 미ì²ë¦¬
í´ê²°ë°©ë²:
// â ëì ì
it('should work', () => {
request(app).get('/users'); // Promise 미ì²ë¦¬
});
// â
ì¢ì ì
it('should work', async () => {
await request(app).get('/users');
});
References
ê³µì 문ì
íìµ ìë£
ë구
- Istanbul/nyc – ì½ë 커ë²ë¦¬ì§
- nock – HTTP 모í¹
- faker.js – í ì¤í¸ ë°ì´í° ìì±
Metadata
ë²ì
- íì¬ ë²ì : 1.0.0
- ìµì¢ ì ë°ì´í¸: 2025-01-01
- í¸í íë«í¼: Claude, ChatGPT, Gemini
ê´ë ¨ ì¤í¬
- api-design: APIì í¨ê» í ì¤í¸ ì¤ê³
- authentication-setup: ì¸ì¦ ìì¤í í ì¤í¸
íê·¸
#testing #backend #Jest #Pytest #unit-test #integration-test #TDD #API-test