9장 가독성

3/4/2025

 Timothy @ Daangn Frontend Core

Software Engineer, Frontend @Daangn Frontend Core Team

ex) Senior Lead Software Engineer @NHN Dooray (2021 ~ 2023)

ex) Lead Software Engineer @ProtoPie (2016 ~ 2021)

ex) Microsoft MVP

Timothy Lee

이 웅재

Intro

  • 단위 테스트의 이름 짓기 규칙

  • 읽기 좋은 테스트 작성

9.1 단위 테스트 이름 짓기

테스트 이름이나 테스트가 포함된 파일 구조에는 다음 세가지 중요한 정보가 포함되어야 한다.

  • 작업 단위의 진입점 (혹은 현재 테스트 중인 기능 이름)

    • ​작업 단위나 진입점의 이름은 테스트를 읽는 사람이 코드의 기능 범위를 쉽게 알 수 있게 해 주기 때문에 상당히 중요하다.

    • 테스트 이름의 첫 부분에 이를 포함하면 테스트 파일에서 쉽게 탐색할 수 있다.

  • 진입점을 테스트하는 상황

  • 작업 단위의 종료점이 실행해야 하는 동작

    • ​무엇을 해야 하거나 반환해야 하는지, 또는 어떻게 동작해야 하는지 알기 쉬운 말로 설명해야 한다.

"진입점 X를 null 값으로 호출하면, 작업 단위의 종료점에서 Y가 수행되어야 한다."

동일한 정보,  다른 표현 방식

test("verifyPassword, with a failing rule, returns error based on rule.reason", () => {
  // ...
});

describe("verifyPassword", () => {
  describe("with a failing rule", () => {
    it("returns error based on the rule.reason", () => {
      // ...
    });
  });
});

verifyPassword_withFailingRule_returnsErrorBasedonRuleReason()

동일한 정보,  다른 표현 방식

  • 중요한 점은  이 세 가지 정보 중 하나라도 빠지면 테스트를 읽는 사람은 테스트를 이해하기 위해 테스트 코드를 읽어야 하므로 소중한 시간을 낭비하게 된다는 것이다.

필수 정보 중 일부가 누락된 테스트 이름

// 무엇을 테스트하는가?
test("failing rule, returns error based on rule.reason", () => {
  // ...
});

// 이것이 언제 발생해야 하는가?
test("verifyPassword, returns error based on rule.reason", () => {
  // ...
});

// 그렇다면 어떤 일이 발생해야 하는가?
test("verifyPassword, with a failing rule", () => {
  // ...
});

명심해야 할 점

  • 코드 가독성을 높여 다른 개발자가 테스트 이름만으로도 무엇을 테스트하는지 알기 쉽게 하는 것

  • 테스트가 실패했을 때 보통 표시되는 유일한 정보가 테스트 이름이기 때문에필수 정보 세 가지를  포함해야 한다. (디버깅 시간과 코드를 읽는 시간을 훨씬 절약할 수 있다.)

  • 좋은 테스트 이름은 문서 역할도 한다. 새로 온 동료가 테스트를 읽고 시스템을 이해할 수 있다면 가독성이 좋다는 것이다. 반대라면 문제가 있다는 것이다.

9.2 매직 넘버와 변수 이름

매직 넘버?

  • "매직"

    • 마치 마법처럼 이 값들이 작동하지만 왜 그렇게 작동하는지 알 수 없다는 의미

  • 매직 문제 (witchcraft value)

  • 하드코딩된 값

  • 기록에 남지 않은 값

  • 명확하게 이해되지 않는 상수나 변수

매직 넘버를 포함한 테스트

describe('password verifier', () => {
  test('on weekends, throws exceptions', () => {
    expect(() => verifyPassword('jhGGu78!', [], 0))
      .toThrowError("It's the weekend!");
  });
});

매직 넘버를 포함한 테스트

  • 매직 넘버

    • ​'jhGGu78!'

    • []

    • 0

매직 넘버 수정하기

describe('verifier2 - dummy object', () => {
  test('on weekends, throws exceptions', () => {
    const SUNDAY = 0, NO_RULES = [];
    expect(() => verifyPassword2('anything', NO_RULES, SUNDAY))
      .toThrowError("It's the weekend!");
  });
});

매직 넘버 수정하기

  • 매직 넘버

    • ​'jhGGu78!' => 'anything' : 아무거나

    • [] => NO_RULES : 비밀번호의 유효성을 검증할 규칙이 없음

    • 0 => SUNDAY : 일요일을 의미하는 숫자

9.3 검증과 실행 단계 분리

가독성!!

  • 가독성을 높이려면 검증 단계실행 단계를 한 문장에 넣지 말아야 한다.

실행 부분과 검증 부분이 한문장

expect(verifier.verify("any value")[0]).toContain("fake reason");

실행 부분과 검증 부분 분리

const result = verifier.verify("any value");

expect(result[0]).toContain("fake reason");

9.4 초기화 및 설정 해제

명심해야 할 점

  • 설정 함수에서 목과 스텁을 설정하면 테스트 내에서는 어디서 만들었는지 찾을 수 없다.

  • 테스트를 읽는 사람은 모의 객체가 사용되고 있다는 사실이나 테스트가 모의 객체가 어떤 값이나 동작을 수행하는지 알지 못할 수 있다.

  • 초기화 함수는 파일 맨 앞에 있지만, 테스트는 그보다 훨씬 아래에 있다.

    • ​어디서 초기화 되었더라?

    • 테스트에서 어떻게 동작할까?

  • ​목은 테스트 내에서 직접 초기화하고 모든 기댓값을 설정하는 것이 훨씬 더 가독성이 좋다.

beforeEach

describe("password verifier", () => {
  let mockLog;
  
  beforeEach(() => {
    mockLog = Substitute.for<IComplicatedLogger>();
  });

  test("verify, with logger & passing, calls logger with PASS", () => {
    const verifier = new PasswordVerifier([], mockLog);
    verifier.verify("anything");

    mockLog.received().info(
      Arg.is((x) => x.includes("PASSED")),
      "verify"
    );
  });
});

beforeEach (x)

describe("password verifier", () => {
  test("verify, with logger & passing, calls logger with PASS", () => {
    const mockLog = Substitute.for<IComplicatedLogger>();

    const verifier = new PasswordVerifier([], mockLog);
    verifier.verify("anything");

    mockLog.received().info(
      Arg.is((x) => x.includes("PASSED")),
      "verify"
    );
  });
});

팩토리 함수를 통한 목 생성

describe("password verifier", () => {
  test("verify, with logger & passing, calls logger with PASS", () => {
    const mockLog = makeMockLogger();

    const verifier = new PasswordVerifier([], mockLog);
    verifier.verify("anything");

    mockLog.received().info(
      Arg.is((x) => x.includes("PASSED")),
      "verify"
    );
  });
});

9장 가독성

By Woongjae Lee

9장 가독성

  • 211