24.09.27

윤지현·2024년 9월 30일

TIL

목록 보기
69/75

1. 테스트 코드와 활용

01. 테스트를 작성해야 하는 이유?

  • 디버깅 감소
    • 테스트를 한 번 작성해두면 프로젝트가 살아 있는 내내 값비싼 결함을 예방해주고, 짜증 나는 디버깅에서 해방시켜준다.
  • 자신 있는 변경
    • 좋은 테스트로 무장한 프로젝트는 자신감을 갖고 변경하고 리팩토링 할 수 있다.
    • 장애가 발생하는 부분을 1차적으로 먼저 걸러냄
  • 더 나은 문서자료
    • 하나의 행위만 집중해 검증하는 명확한 테스트는 마치 실행 가능한 문서와 같다.
  • 더 단순한 리뷰
    • 리뷰어가 변경된 코드가 제대로 작동하는지 검증하는 시간을 줄여준다.
  • 사려 깊은 설계
    • 새로 작성한 코드의 테스트를 작성하는 일은 실질적으로 해당 코드의 API가 잘 설계되었는지를 시험하는 행위이다.
  • 고품질의 릴리스를 빠르게
    • 건실한 자동 테스트 스위트를 갖춘 팀은 새로운 버전을 릴리즈하며 불안에 떨지 않는다.

솔직히 말로는 이렇게 하는데 내가 생각하기에는 '미래의 나'를 위해서 작성하는 것 같다.

초반에는 프로젝트에 기능이 20개 정도 되니까 가능한데 계속 개발되서 기능이 300개로 늘어나게 되면 그걸 전부 테스트를 해야하기 때문에 너무 힘들기 때문에 작성하게 된다.


02. 테스트 이론

단위 테스트와 통합 테스트

  • 단위 테스트 : 가장 작은 단위의 테스트 방식. 메서드 단위로 테스트를 수행하고, 의도한 결과값이 나오는지 확인하는 수준 (ex: controller, service, repository… 격리해서 따로따로)
  • 통합 테스트 : 어플리케이션이 정상적으로 동작하는지 확인. 여러모듈을 함께 테스트 (ex: controller 메소드를 호출하면 controller → service→ repository 모두 수행)

TDD (Test Driven Development)

  • 개발 할 때 테스트 코드를 먼저 작성하고 해당 테스트를 성공 시키게 만들면서 리팩토링 하는 개발 기법
    - 순서
    - 원하는 행위를 먼저 테스트 코드에 작성한다. → 실제 코드가 없기에 당연히 실패함
    - 실제 코드를 원하는 행위를 할 수 있게 코드를 작성함 → 개발이 완료되면 테스트 코드가 성공
    - 2번에서 작성한 코드를 가독성 및 유지보수가 쉽도록 리팩토링
    - 리팩토링 완료 후 테스트 실행하여 성공 여부 확인
    -- BUT!!! --
  • TDD를 하면 버그가 없나요? No!
  • TDD는 모두한테 필요한가요? No!

💡 TDD가 모든 것의 정답인 것 처럼 생각 할 수 있는데
개인적으로 TDD 개발론에 대해서 회의적이라고 생각

  • 위의 방식대로 개발하면 개발속도가 매우 떨어진다. 그래서 모든 프로젝트에서 TDD를 적용하는 것은 좋지 못하다고 생각

F.I.R.S.T 원칙

💡 좋은 테스트를 위한 F.I.R.S.T 원칙

  • Fast : 단위 테스트는 빨라야 한다. (가장 중요~)
  • Independent : 단위 테스트는 각각의 테스트가 연관되지 않고, 고립되어야 한다. 즉 독립적
    이어야 한다.
  • Repeatable : 단위테스트는 반복 가능해야한다. 반복적으로 시행해도 결과는 같아야 한다.
  • Self-validating : 자체적으로 테스트 결과가 도출되야한다. (개발자가 print 찍어서 비교하는게 아니고 자동으로 수행되며 성공/실패 의 결과가 나오는 것)
  • Timely : 단위 테스트는 실제 코드보다 먼저 구현해야한다. (TDD일 경우에만 해당)

Given-When-Then 패턴

💡 Test 실행을 위한 Given-When-Then 패턴

  • Given : 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황 또는 행동을 정의합니다.
  • When: 실제 테스트를 하는 메소드가 호출되며 테스트를 통한 결과값을 가져옵니다.
  • Then: When 단계에서 나온 결과값을 검증하는 작업을 수행합니다.

2. 생산성을 높여주는 단위 테스트

단위 테스트

  • 일반적으로 작고, 범위가 좁은 테스트를 의미한다.
  • 테스트의 가장 중요한 목적
    • 버그 예방
    • 엔지니어의 생산성 개선

유지보수하기 쉬운 단위 테스트 작성

테스트는 개발자가 믿을 수 있을 때 의미다. 자주 깨져 생산성을 저하시키는 테스트는 오히려 독이 된다.
---- 유지보수 하기 어려운 테스트(엔지니어의 생산성을 저하시키는 테스트)는 다음 2가지 특징을 가지고 있다. ------

  • 깨지기 쉬운 테스트 : 검증 대상과 관련 없는 변경 때문에 실패하는 테스트
    • 만약 유저수정을 테스트 하고 싶은데, 유저 생성에서 오류가 발생해서 실패
  • 불명확한 테스트 : 무엇이 잘못되어 실패했는지, 어떻게 고쳐야 하는지를 파악하기 어려운 테스트

깨지기 쉬운 테스트 예방하기 ( 테스트 하고 싶은 것 만 테스트, 많은 로직 넣지 않기 )

  • 이상적인 테스트

    • 한 번 작성한 후로는 대상 시스템의 요구사항이 바뀌지 않는 한 절대 수정할 일이 없어야 한다.
    • ex)
      • 순수 리팩터링(외부 인터페이스는 놔두고 내부만 리팩터링하는 경우) → 테스트는 변경되지 않아야한다.
      • 새로운 기능 추가 → 테스트는 변경되지 않아야 한다. & 새로운 테스트 추가
      • 버그 수정 → 기존 테스트 변경 X & 누락된 테스트 추가
      • 행위 변경 → 기존 테스트 변경
  • 과정이 아닌 결과에 초점을 맞춘다.

    • example
          public void processTransaction(Transaction transaction) {
        // 돈을 보내는 코드
            if (isValid(transaction) { // 유효하면
                saveToDatabase(transaction); // DB에 저장
            }
        }
    
        private boolean isValid(Transactiont t) { // 유효한지 검새
            return t.getAmount() < t.getSender().getBalance();
        }
    
        private void saveToDatabase(Transaction t) {  // DB에 저장
            String s = t.getSender() + "," + t.getReceipient() + "," + t.getAmount();
            database.put(t.getId(), s);
        }
    
        public void setAccountBalance(String accountName, int balance) {
            // 잔고를 데이터베이스에 직접 기록한다.
        }
    
        public void getAccountBalance(String accountName) {
            // 계좌 잔고를 확인하기 위해 데이터베이스로부터 거래 정보를 읽어온다.
        }
  • Best Practice [결과에 초점을 맞춤]

    • 현실적이라 잘 깨지지 않는 테스트다.
    • 실제로 행위가 변경될 경우에만 테스트가 깨진다.
         @Test
      // 돈이 잘 전송되었는지 테스트
      public void shouldTransferFunds() {
          processor.setAccountBalance("me", 150);
          processor.setAccountBalance("you", 20);

          processor.processTransaction(newTransaction()
              .setSender("me")
              .setRecipient("you")
              .setAmount(100));

      // 결론적으로 나한테 돈이 50이 있고 너한테 돈이 120이 있는지 검사
          assertThat(processor.getAccountBalance("me")).isEqualTo(50);
          assertThat(processor.getAccountBalance("you")).isEqualTo(120);
      }

      @Test
      // 잔액 부족시 돈이 전송이 되는지 테스트
      public void shouldNotPerformInvalidTransactions() {
          processor.setAccountBalance("me", 50);
          processor.setAccountBalance("you", 20);

          processor.processTransction(newTransaction()
              .setSender("me")
              .setReceipient("you")
              .setAmount(100));

      // 결론적으로 나한테 돈이 50이 있고 너한테 돈이 20이 있는지 검사
      // 사실 이건 processTransction 내부의 isValid 통과를 못해서 수행이 안될 것임
      // 하지만 isValid 메서드 명이 변경이 되더라도 다른 검증 로직이 추가되더라도
      // 테스크 코드는 수정될 필요 없이 실패 했다고 나올 것 임
          assertThat(processor.getAccountBalance("me").isEqualTo(50);
          assertThat(processor.getAccountBalance("you").isEqualTo(20);
      }

  • 상호작용이 아니라 상태를 테스트하기
    • 상태 테스트 : 메소드 호출 후 시스템 자체를 관찰한다.
    • 상호작용 테스트 : 호출을 처리하는 과정에서 시스템이 다른 모듈들과 협력하여 기대한 일련의 동작을 수행하는지를 확인 한다.
    • 대체로 상호작용 테스트는 상태 테스트보다 깨지기 쉽습니다. 왜냐하면 내부 구현에 의존하기 때문입니다.
  • 상태 테스트
@Test
public void shouldCreateUser() {
	accounts.createUser("foobar");
	assertThat(account.getUser("foobar").isNotNull);
}

명확한 테스트 작성하기

테스트가 실패한다는 건 엔지니어에게 유용한 신호를 주는 것이며, 테스트의 존재 가치를 증명하는 가장 주요한 수단 중 하나입니다.

  • 테스트 실패하는 케이스
    • 대상 시스템에 문제가 있거나 불완전함. (테스트가 자신의 역할을 하고 있는 상황)
    • 테스트 자체에 결함이 있는 경우. 이 때 대상 시스템에는 아무런 문제가 없다. (테스트가 깨지기 쉬운 테스트의 경우)

따라서 테스트가 깨지면 위 2 케이스 중 어떤 케이스인지 빠르게 판단해야 합니다. 그리고 이 일을 얼마나 빠르게 마치느냐는 테스트의 명확성에 달렸습니다.

  • 명확한 테스트 : 존재 이유와 실패 원인을 엔지니어가 곧바로 알아 차릴 수 있는 테스트
  • 완전하고 간결하게 만들기
    • 완전한 테스트 : 결과에 도달하기까지의 논리를 읽는 이가 이해하는 데 필요한 모든 정보를 본문에 담고 있는 테스트
    • 간결한 테스트 : 코드가 산만하지 않고, 관련 없는 정보는 포함하지 않은 테스트
  • 완전하고 간결한 테스트
@Test
public void shouldPerformAddition() {
	Calculator calculator = new Calculator();
	int result = calculator.calculator(newCalculation(2, Operation.PLUS, 3)));
	assertThat(result).isEqualTo(5);
}

3. 그냥 통합 테스트

통합 테스트

  • 일반적으로 크고, 범위가 넓은 테스트를 의미한다.
  • 테스트의 가장 중요한 목적
    • 단위테스트만으로는 시스템이 전체적으로 잘 작동하는지 확신할 수 없습니다.
    • 외부와 단절된 상태로 확인하기 때문에 각 부분이 데이터베이스나 Redis 등의 외부 시스템과 어떻게 통합되는지도 확인해봐야 합니다.
    • 쉽게 생각하면 우리가 지금까지 테스트 했던 Postman 호출 테스트라고 생각하면 편합니다.

4. 결론

  1. 안정적으로 서비스를 개발하기 위해선 테스트는 필수
  2. 테스트를 작성할 때 가장 중요한 것은 개발자가 믿을 수 있는 테스트, 개발자의 생산성을 증가시켜주는 테스트를 작성해야 한다.
    • 깨지지 않는 테스트 & 명확한 테스트를 작성해야 한다.
  3. Repository, Service, Controller에 작성되어 있는 다른 메소드들에 대한 성공 테스트 코드를 작성하기.
  4. 실패하는 테스트 시나리오를 작성해 테스트를 만들기.
profile
첫 시작

0개의 댓글