Node.js 인터뷰 질문 97
질문: Node.js 애플리케이션의 효과적인 테스트 전략과 테스트 자동화 방법에 대해 설명해주세요.
답변:
Node.js 애플리케이션의 테스트 전략과 자동화 방법을 살펴보겠습니다.
1. 단위 테스트 구현
// user-service.test.js
const { expect } = require("chai");
const sinon = require("sinon");
const UserService = require("./user-service");
const UserRepository = require("./user-repository");
describe("UserService", () => {
let userService;
let userRepository;
beforeEach(() => {
userRepository = new UserRepository();
userService = new UserService(userRepository);
});
afterEach(() => {
sinon.restore();
});
describe("createUser", () => {
it("유효한 사용자 데이터로 사용자를 생성해야 함", async () => {
// 준비
const userData = {
email: "test@example.com",
password: "password123",
name: "Test User",
};
const expectedUser = { ...userData, id: 1 };
sinon.stub(userRepository, "create").resolves(expectedUser);
// 실행
const result = await userService.createUser(userData);
// 검증
expect(result).to.deep.equal(expectedUser);
expect(userRepository.create.calledOnce).to.be.true;
expect(userRepository.create.calledWith(userData)).to.be.true;
});
it("잘못된 이메일로 사용자 생성 시 오류를 발생시켜야 함", async () => {
// 준비
const invalidUserData = {
email: "invalid-email",
password: "password123",
name: "Test User",
};
// 실행 & 검증
await expect(userService.createUser(invalidUserData)).to.be.rejectedWith(
"유효하지 않은 이메일 형식입니다."
);
});
});
describe("authenticateUser", () => {
it("올바른 자격 증명으로 사용자를 인증해야 함", async () => {
// 준비
const credentials = {
email: "test@example.com",
password: "password123",
};
const user = {
id: 1,
email: credentials.email,
password: "$2b$10$...", // 해시된 비밀번호
};
sinon.stub(userRepository, "findByEmail").resolves(user);
sinon.stub(userService, "verifyPassword").resolves(true);
// 실행
const result = await userService.authenticateUser(
credentials.email,
credentials.password
);
// 검증
expect(result).to.deep.equal(user);
expect(userRepository.findByEmail.calledOnce).to.be.true;
expect(userService.verifyPassword.calledOnce).to.be.true;
});
});
});
2. 통합 테스트 구현
// api-integration.test.js
const request = require("supertest");
const { expect } = require("chai");
const app = require("../app");
const db = require("../db");
describe("API 통합 테스트", () => {
before(async () => {
// 데이터베이스 초기화
await db.migrate.latest();
});
after(async () => {
// 테스트 데이터 정리
await db.migrate.rollback();
});
describe("POST /api/users", () => {
beforeEach(async () => {
// 테스트 데이터 초기화
await db("users").truncate();
});
it("새로운 사용자를 생성해야 함", async () => {
const userData = {
email: "test@example.com",
password: "password123",
name: "Test User",
};
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(201);
expect(response.body).to.have.property("id");
expect(response.body.email).to.equal(userData.email);
expect(response.body.name).to.equal(userData.name);
// 데이터베이스 확인
const user = await db("users").where("email", userData.email).first();
expect(user).to.exist;
expect(user.email).to.equal(userData.email);
});
it("중복된 이메일로 사용자 생성 시 409를 반환해야 함", async () => {
const userData = {
email: "test@example.com",
password: "password123",
name: "Test User",
};
// 첫 번째 사용자 생성
await request(app).post("/api/users").send(userData);
// 동일한 이메일로 두 번째 사용자 생성 시도
const response = await request(app)
.post("/api/users")
.send(userData)
.expect(409);
expect(response.body).to.have.property("error");
expect(response.body.error).to.include("이미 존재하는 이메일");
});
});
});
3. E2E 테스트 구현
// e2e.test.js
const { chromium } = require("playwright");
const { expect } = require("chai");
describe("E2E 테스트", () => {
let browser;
let page;
before(async () => {
browser = await chromium.launch();
});
after(async () => {
await browser.close();
});
beforeEach(async () => {
page = await browser.newPage();
await page.goto("http://localhost:3000");
});
afterEach(async () => {
await page.close();
});
it("로그인 및 대시보드 접근 테스트", async () => {
// 로그인 페이지 접근
await page.click("text=로그인");
// 로그인 폼 작성
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
// 대시보드로 리다이렉트 확인
await page.waitForURL("**/dashboard");
// 대시보드 내용 확인
const welcomeText = await page.textContent("h1");
expect(welcomeText).to.include("환영합니다");
});
it("사용자 프로필 업데이트 테스트", async () => {
// 로그인
await performLogin(page);
// 프로필 페이지 이동
await page.click("text=프로필");
// 프로필 정보 업데이트
await page.fill('input[name="name"]', "Updated Name");
await page.click('button:text("저장")');
// 성공 메시지 확인
const toast = await page.waitForSelector(".toast-success");
expect(await toast.textContent()).to.include("프로필이 업데이트되었습니다");
// 변경사항 지속성 확인
await page.reload();
const nameInput = await page.$('input[name="name"]');
expect(await nameInput.inputValue()).to.equal("Updated Name");
});
});
async function performLogin(page) {
await page.goto("http://localhost:3000/login");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForURL("**/dashboard");
}
4. 성능 테스트 구현
// performance.test.js
const autocannon = require("autocannon");
const { expect } = require("chai");
describe("성능 테스트", () => {
it("API 엔드포인트가 초당 1000개 이상의 요청을 처리해야 함", async () => {
const result = await autocannon({
url: "http://localhost:3000/api/users",
connections: 10,
duration: 10,
amount: 10000,
headers: {
"Content-Type": "application/json",
},
});
expect(result.requests.average).to.be.above(1000);
expect(result.latency.average).to.be.below(50);
});
it("데이터베이스 쿼리가 100ms 이내에 완료되어야 함", async () => {
const startTime = process.hrtime();
await db.select("*").from("users").limit(1000);
const [seconds, nanoseconds] = process.hrtime(startTime);
const milliseconds = seconds * 1000 + nanoseconds / 1000000;
expect(milliseconds).to.be.below(100);
});
});
5. 테스트 자동화 설정
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
setupFilesAfterEnv: ['./jest.setup.js']
};
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:e2e": "playwright test",
"test:integration": "mocha test/integration/**/*.test.js",
"test:performance": "node test/performance/run.js"
}
}
// github-actions-test.yml
name: 테스트 자동화
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Node.js 설정
uses: actions/setup-node@v2
with:
node-version: '16'
- name: 의존성 설치
run: npm ci
- name: 린트 검사
run: npm run lint
- name: 단위 테스트
run: npm test
- name: 통합 테스트
run: npm run test:integration
- name: E2E 테스트
run: npm run test:e2e
- name: 커버리지 리포트
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
요약
Node.js 테스트 전략의 주요 구성 요소:
단위 테스트
- 개별 함수/모듈 테스트
- 모의 객체 활용
- 경계 조건 검증
통합 테스트
- API 엔드포인트 테스트
- 데이터베이스 연동 테스트
- 외부 서비스 통합 테스트
E2E 테스트
- 사용자 시나리오 테스트
- UI 상호작용 테스트
- 전체 흐름 검증
성능 테스트
- 부하 테스트
- 응답 시간 측정
- 병목 현상 분석
테스트 자동화
- CI/CD 파이프라인
- 테스트 커버리지
- 자동화된 보고서