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 테스트 전략의 주요 구성 요소:

  1. 단위 테스트

    • 개별 함수/모듈 테스트
    • 모의 객체 활용
    • 경계 조건 검증
  2. 통합 테스트

    • API 엔드포인트 테스트
    • 데이터베이스 연동 테스트
    • 외부 서비스 통합 테스트
  3. E2E 테스트

    • 사용자 시나리오 테스트
    • UI 상호작용 테스트
    • 전체 흐름 검증
  4. 성능 테스트

    • 부하 테스트
    • 응답 시간 측정
    • 병목 현상 분석
  5. 테스트 자동화

    • CI/CD 파이프라인
    • 테스트 커버리지
    • 자동화된 보고서

results matching ""

    No results matching ""