Node.js 인터뷰 질문 89
질문: Node.js 애플리케이션에서 테스트를 구현하는 다양한 방법과 테스트 전략에 대해 설명해주세요.
답변:
Node.js 애플리케이션의 테스트는 코드의 품질과 신뢰성을 보장하는 중요한 부분입니다. 단위 테스트, 통합 테스트, E2E 테스트 등 다양한 테스트 전략을 살펴보겠습니다.
1. 단위 테스트 (Jest 사용)
Jest는 Facebook에서 개발한 JavaScript 테스트 프레임워크로, Node.js 애플리케이션 테스트에 널리 사용됩니다.
// user.service.js
class UserService {
constructor(userRepository) {
this.userRepository = userRepository;
}
async createUser(userData) {
if (!userData.email || !userData.password) {
throw new Error("이메일과 비밀번호는 필수입니다");
}
const existingUser = await this.userRepository.findByEmail(userData.email);
if (existingUser) {
throw new Error("이미 존재하는 이메일입니다");
}
return this.userRepository.create(userData);
}
async getUserById(id) {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error("사용자를 찾을 수 없습니다");
}
return user;
}
}
// user.service.test.js
describe("UserService", () => {
let userService;
let mockUserRepository;
beforeEach(() => {
// 목 리포지토리 설정
mockUserRepository = {
findByEmail: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
};
userService = new UserService(mockUserRepository);
});
describe("createUser", () => {
it("유효한 데이터로 사용자를 생성해야 함", async () => {
const userData = {
email: "test@example.com",
password: "password123",
};
mockUserRepository.findByEmail.mockResolvedValue(null);
mockUserRepository.create.mockResolvedValue({
id: 1,
...userData,
});
const result = await userService.createUser(userData);
expect(result).toHaveProperty("id", 1);
expect(result.email).toBe(userData.email);
expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
});
it("이메일이 없으면 오류를 발생시켜야 함", async () => {
const userData = {
password: "password123",
};
await expect(userService.createUser(userData)).rejects.toThrow(
"이메일과 비밀번호는 필수입니다"
);
});
it("이미 존재하는 이메일이면 오류를 발생시켜야 함", async () => {
const userData = {
email: "existing@example.com",
password: "password123",
};
mockUserRepository.findByEmail.mockResolvedValue({ id: 1, ...userData });
await expect(userService.createUser(userData)).rejects.toThrow(
"이미 존재하는 이메일입니다"
);
});
});
});
2. 통합 테스트 (Supertest 사용)
Supertest를 사용하여 HTTP API 엔드포인트를 테스트합니다.
// app.js
const express = require("express");
const app = express();
app.use(express.json());
app.post("/api/users", async (req, res) => {
try {
const user = await userService.createUser(req.body);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// app.test.js
const request = require("supertest");
const app = require("./app");
const { connectDB, closeDB } = require("./db");
describe("User API", () => {
beforeAll(async () => {
await connectDB();
});
afterAll(async () => {
await closeDB();
});
describe("POST /api/users", () => {
it("유효한 데이터로 사용자를 생성해야 함", async () => {
const userData = {
email: "test@example.com",
password: "password123",
};
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(201);
expect(response.body).toHaveProperty("id");
expect(response.body.email).toBe(userData.email);
});
it("유효하지 않은 데이터로 400 에러를 반환해야 함", async () => {
const userData = {
password: "password123",
};
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(400);
expect(response.body).toHaveProperty("error");
});
});
});
3. E2E 테스트 (Cypress 사용)
Cypress를 사용하여 전체 애플리케이션 흐름을 테스트합니다.
// cypress/integration/user-flow.spec.js
describe("사용자 흐름", () => {
beforeEach(() => {
cy.visit("/");
});
it("회원가입 및 로그인 흐름을 테스트", () => {
// 회원가입
cy.get('[data-test="signup-link"]').click();
cy.get('[data-test="email-input"]').type("test@example.com");
cy.get('[data-test="password-input"]').type("password123");
cy.get('[data-test="signup-button"]').click();
// 회원가입 성공 확인
cy.get('[data-test="success-message"]')
.should("be.visible")
.and("contain", "회원가입이 완료되었습니다");
// 로그인
cy.get('[data-test="login-link"]').click();
cy.get('[data-test="email-input"]').type("test@example.com");
cy.get('[data-test="password-input"]').type("password123");
cy.get('[data-test="login-button"]').click();
// 로그인 성공 확인
cy.get('[data-test="user-profile"]')
.should("be.visible")
.and("contain", "test@example.com");
});
});
4. 목(Mock)과 스텁(Stub) 사용
외부 의존성을 테스트에서 격리하기 위한 목과 스텁 사용:
// payment.service.js
class PaymentService {
constructor(paymentGateway) {
this.paymentGateway = paymentGateway;
}
async processPayment(amount, cardDetails) {
try {
const result = await this.paymentGateway.charge(amount, cardDetails);
return {
success: true,
transactionId: result.id,
};
} catch (error) {
return {
success: false,
error: error.message,
};
}
}
}
// payment.service.test.js
describe("PaymentService", () => {
let paymentService;
let mockPaymentGateway;
beforeEach(() => {
// 목 페이먼트 게이트웨이 생성
mockPaymentGateway = {
charge: jest.fn(),
};
paymentService = new PaymentService(mockPaymentGateway);
});
it("결제가 성공적으로 처리되어야 함", async () => {
const amount = 1000;
const cardDetails = {
number: "4111111111111111",
expiry: "12/24",
cvv: "123",
};
mockPaymentGateway.charge.mockResolvedValue({
id: "tx_123",
status: "succeeded",
});
const result = await paymentService.processPayment(amount, cardDetails);
expect(result.success).toBe(true);
expect(result.transactionId).toBe("tx_123");
expect(mockPaymentGateway.charge).toHaveBeenCalledWith(amount, cardDetails);
});
it("결제 실패 시 에러를 반환해야 함", async () => {
mockPaymentGateway.charge.mockRejectedValue(
new Error("카드가 거절되었습니다")
);
const result = await paymentService.processPayment(1000, {});
expect(result.success).toBe(false);
expect(result.error).toBe("카드가 거절되었습니다");
});
});
5. 테스트 커버리지 분석
Jest의 테스트 커버리지 도구 사용:
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/index.js",
"!src/config/*.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
6. 성능 테스트
Artillery를 사용한 부하 테스트:
# performance-test.yml
config:
target: "http://localhost:3000"
phases:
- duration: 60
arrivalRate: 20
name: "Warm up"
- duration: 120
arrivalRate: 50
name: "Peak load"
scenarios:
- name: "API 엔드포인트 테스트"
flow:
- get:
url: "/api/users"
- think: 1
- post:
url: "/api/users"
json:
email: "test@example.com"
password: "password123"
7. 테스트 환경 설정
테스트 환경별 설정 관리:
// config/test.js
module.exports = {
database: {
url: "mongodb://localhost:27017/test_db",
},
jwt: {
secret: "test_secret",
},
mail: {
// 테스트용 메일 설정
transport: "smtp",
host: "localhost",
port: 1025,
},
};
// test/setup.js
const config = require("../config/test");
const { MongoMemoryServer } = require("mongodb-memory-server");
let mongoServer;
beforeAll(async () => {
// 인메모리 MongoDB 서버 시작
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
process.env.MONGODB_URI = mongoUri;
});
afterAll(async () => {
// 테스트 후 정리
await mongoServer.stop();
});
// 각 테스트 전에 데이터베이스 초기화
beforeEach(async () => {
await clearDatabase();
});
요약
Node.js 애플리케이션 테스트 전략의 주요 포인트:
단위 테스트
- 개별 컴포넌트 테스트
- Jest 사용
- 목과 스텁 활용
통합 테스트
- API 엔드포인트 테스트
- 데이터베이스 연동 테스트
- Supertest 활용
E2E 테스트
- 전체 사용자 흐름 테스트
- Cypress 사용
- 실제 환경과 유사한 조건
테스트 환경
- 환경별 설정 관리
- 테스트 데이터베이스 사용
- 외부 서비스 목 처리
테스트 커버리지
- 코드 커버리지 측정
- 임계값 설정
- 지속적인 모니터링
성능 테스트
- 부하 테스트
- 확장성 검증
- 병목 지점 식별
테스트 자동화
- CI/CD 파이프라인 통합
- 자동화된 테스트 실행
- 빠른 피드백 루프