Clean Code & Refactoring

[Clean Code] 9장. 단위 테스트 : Unit Tests

유자맛바나나 2022. 3. 9. 05:37

[Clean Code 시리즈 포스팅]

[Clean Code] 1장. 깨끗한 코드: Clean Code

[Clean Code] 2장. 의미 있는 이름 : Meaningful Names

[Clean Code] 3장. 함수 : Functions

[Clean Code] 4장. 주석 : Comments

[Clean Code] 5장. 형식 맞추기 : Formatting

[Clean Code] 6장. 객체와 자료구조 : Objects and Data Structures

[Clean Code] 9장. 단위 테스트 : Unit Tests (Now)

 

 

❑ TDD 법칙 세가지

첫째 법칙

실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다

둘째 법칙

컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다

셋째 법칙

현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다

위 세가지 규칙을 따르면 개발과 테스트가 약 30초 주기로 묶인다. 테스트 코드와 실제 코드가 함께 오며 테스트 코드가 실제 코드보다 불과 몇 초 전에 나온다.

 

 

❑ 깨끗한 테스트 코드 유지하기

"테스트 코드는 실제 코드 못지 않게 중요하다"
  • 일회용 테스트 코드를 작성하다 자동화된 단위 테스트 슈트를 작성하는 것은 어려운 일이다.
  • "없는 것보다 낫다" 라는 생각으로 단위 테스트를 '지저분하게' 작성하다 보면, 아래와 같은 굴레에 빠지게 된다.
    1. 실제 코드가 변하면 테스트 코드가 변하게 되고, 지저분한 테스트 코드를 이해하고 변경하는데 많은 시간이 소요된다
    2. 새 버전을 출시할 때 마다 테스트 케이스를 유지보수하는데 들어가는 비용이 늘어난다
    3. 점차 개발자 사이에서 테스트 코드는 불만으로 전락한다
    4. 결국 테스트 슈트를 폐기하는 상황이 된다
    5. 하지만 테스트 슈트가 없으면 부작용이 발생하기 시작한다. 자신이 수정한 코드가 제대로 작동하는지 확인할 방법이 없으며, 수정한 코드로 인해 발생하는 다른 코드에서의 사이드 이펙트를 검증하지 못한다
    6. 부작용이 늘어남에 따라 시스템의 결함율이 높아진다. 의도하지 않은 결함이 늘어나면 개발자는 변경을 주저하며 더 이상 코드를 정리하지 않는다
    7. 그렇게 코드가 망가지게 된다
  • 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 명쾌하다. 테스트 케이스가 있으면 변경이 두렵지 않기 때문이다

 

❑ 깨끗한 테스트 코드

깨끗한 테스트 코드를 만들려면 세 가지가 필요하다. 가독성, 가독성, 가독성.

테스트 코드에서 가독성을 높이는 것은 여느 코드와 마찬가지다. 명료성, 단순성, 풍부한 표현력이 필요하다

 

1. BUILD-OPERATE-CHECK 패턴

  • BUILD-OPERATE-CHECK 패턴을 활용한 단위 테스트 작성
    테스트 구조를 세 개의 부분으로 구분한 것을 말한다. 대부분의 테스트 케이스가 이에 해당할 것으로 생각한다.
    1. BUILD: 테스트 자료 만들기
    2. OPERATE: 테스트 자료를 조작(전처리)
    3. CHECK: 조작한 결과가 올바른지 확인
  • BUILD-OPERATE-CHECK 패턴을 활용한 예시
    • Before Refactoring 테스트 코드의 각 부분을 After의 BUILD, OPERATE, CHECK로 분리하며 테스트 코드가 매우 간단하고 명료해졌다. 이를 통해 가독성을 확보했다고 볼 수 있다.

Before Refactoring

public class BuildOperateCheckPatternTest {

    @Test
    public void drivableTestBeforeRafactoring() throws Exception {
        Car car = null;
        String engineType = "gasoline";
        if (engineType.equals("gasoline")) {
            car = new GasolineCar();
        } else if (engineType.equals("diesel")) {
            car = new DieselCar();
        } else if (engineType.equals("electric")) {
            car = new ElectricCar();
        }

        car.changeDoorStatus("open");
        car.checkBattery();
        car.pushBreak();
        car.turnOnEngine();

        assertTrue(car.isEngineOn());
        assertTrue(car.isEngineOilPumpOn());
        assertTrue(car.isDisplayOn());
    }
}

After Refactoring

public class BuildOperateCheckPatternTest {

    @Test
    public void drivableTestAfterRafactoring() throws Exception {
        // BUILD
        Car car = buildCar("gasoline");

        // OPERATE
        operateCarDrivable(car);

        // CHECK
        assertCarIsDrivable(car);
    }

    public Car buildCar(String engineType) {
        Car car = null;
        if (engineType.equals("gasoline")) {
            car = new GasolineCar();
        } else if (engineType.equals("diesel")) {
            car = new DieselCar();
        } else if (engineType.equals("electric")) {
            car = new ElectricCar();
        }
        return car;
    }

    public void operateCarDrivable(Car car) {
        car.changeDoorStatus("open");
        car.checkBattery();
        car.pushBreak();
        car.turnOnEngine();
    }

    public void assertCarIsDrivable(Car car) {
        assertTrue(car.isEngineOn());
        assertTrue(car.isEngineOilPumpOn());
        assertTrue(car.isDisplayOn());
    }
}

 

2. 이중 표준

  • 테스트 API 코드에 적용하는 표준은 실제 표준과 다르다는 뜻이다.
  • 요약하자면 실제 코드만큼 엄격하고 효율적일 필요는 없다는 뜻이다. 테스트 환경에서 작동되는 코드이기 때문이다.
  • 하지만 엄격한 것과 효율적인 것의 기준은 사람마다 다를 것으로 생각하기 때문에 팀에서 적절히 합의하는게 좋을 것으로 보인다.

 

❑ 테스트 당 assert 하나(단일 assert 문)

1. '단일 assert 문' 보단 'assert문 최소화'가 더 좋다

  • 테스트 당 assert 하나만 넣는 '단일 assert문 테스트 함수'은 가혹한 규칙일 수 있으나 결론이 하나라서 코드를 이해하기 쉽고 빠르다
  • 하지만 이는 하나의 테스트 함수 안에 작성할 수 여러 개의 assert문을 분리했다는 뜻이고, 중복되는 코드가 발생할 가능성이 높다
  • 따라서 저자는 '단일 assert 문'이 훌륭한 지침이지만 중복 코드를 발생시킬 수 있기에 assert 문을 최대한 줄이는 것을 지향하는 것이 좋다고 말한다.

2. '테스트 당 개념 하나'의 원칙으로 작성하자

  • assert문 최소화와 더불어 '테스트 당 개념 하나'라는 규칙이 더 낫다고 한다. 잡다한 개념을 연속으로 테스트하는 긴 함수는 피해야한다.

 

❑ F.I.R.S.T

깨끗한 테스트는 다음 다섯가지 규칙을 따른다

1. Fast: 빠르게

  • 테스트는 빨리 돌아야 한다는 뜻이다
  • 테스트가 느리면 자주 돌릴 엄두를 못내고, 자주 돌리지 않으면 초반에 문제를 찾아낼 수 없다
  • 결국 코드 품질이 망가지기 시작한다

2. Independent: 독립적으로

  • 각 테스트는 서로 의존하면 안된다
  • 하나의 테스트가 다음 테스트를 실행되기 위한 환경을 준비해선 안되고 각각 독립적으로, 어떤 순서로 실행해도 괜찮아야 한다.
  • 테스트가 서로 의존하면 하나가 실패할 때 나머지도 실패하므로 원인 진단이 어려워 진다.

3. Repeatable: 반복 가능하게

  • 테스트는 어떤 환경에서도 반복 가능해야 한다
  • 실제 환경, QA 환경, 오프라인 환경등 어떤 환경에서도 가능해야 한다는 뜻이다
  • 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패할 수 밖에 없다고 변명할 여지가 생기며, 실제로 테스트가 수행되지 못하는 상황이 발생한다

4. Self-Validating: 자가 검증하는

  • 테스트는 Bool 값으로 결과를 내야 한다. 성공 아니면 실패다.
  • 통과 여부를 알기 위해 로그 파일을 읽어선 안된다. 통과 여부를 알기 위해 텍스트 파일을 수작업으로 비교해서도 안된다.
  • 테스트 스스로 성공과 실패를 파악하지 못한다면 판단은 주관적이게 되며, 지루한 수작업 평가가 수반된다.

5. Timely: 적시에

  • 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.
  • 실제 코드를 구현한 후 테스트 코드를 작성한다면 테스트 하기 어렵다는 사실을 발견할지도 모른다.
  • 결국 테스트가 불가능하도록 실제 코드가 설계될 수 있다.

 

❑ Reference

클린 코드 | 로버트 C.마틴 | 인사이트