본문 바로가기
Back-end/테스트

테스트와 테스트 주도 개발 TDD

by javapp 자바앱 2024. 6. 27.
728x90

 

 

 

테스트

 

테스트의 장점

  • 회귀 방지: 기존코드가 변경에도 잘 실행되는지 확인가능 
  • 잠재적 오류의 발견 가능성이 높음
  • 테스트를 통과한 코드만 운영코드에 배포 

 

테스트 종류

  • 인수테스트: 최종 환경과 유사한 상태로 테스트
  • E2E테스트: End To End Test 처음부터 끝까지 테스트
  • 통합테스트: 여러 요소를 복합적으로 테스트
  • 단위테스트: 필요한 단위로 쪼개서 테스트

 

 

테스트 대역

모든 유형의 비운영용 가짜 의존성을 포괄하는 용어,
스턴트 대역이라는 개념에서 시작

관리할 수 없는 것들에 주로 Mock 사용

 

 

 

 

단위 테스트

DB 포함해서 단위테스트를 작성할 때 신경쓸 부분

 

테스트간에 데이터가 공유되어서는 안됩니다!

- 테스트간 독립적으로 수행되고 서로의 영향을 받지 않도록 설정하는 것이 핵심!

 

텍스트 픽스처:

- setup, beforeeach 등으로 테스트에서 사용할 픽스처를 만들어서 사용

- 공유 될 수 있기에 내부에서만 사용할 수 있도록 처리

private User createUser() { .... }

 

의존성을 대체하지 않는 경우에 발생할 수 있는 이슈

- 테스트 대상의 문제가 아닌 이유로 테스트를 통과하지 못할 수 있다.

--> 텍스트 픽스처로 해결 가능

 

 

 

통합테스트

  • 단위테스트 != 통합테스트
  • 외부 의존성을 포함한 컨트롤러 영역
  • 통합테스트: 하나의 단위를 초과하여 검증하는 테스트, 의존성을 갖는 테스트 (= 이메일, 데이터베이스 등)
  • 관리하지 못하는 의존성에 대해서 테스트 대역을 적극적으로 활용 
  • 단위들이 합쳐서 발생하는 예외들을 테스트 
  • 상호작용이 필요한 영역

 

 

E2E테스트

  • 배포된 API 를 통해서 테스트를 진행 
  • 처음부터 끝까지 모든 의존성을 포함한 테스트
  • QA로 수행하는 테스트를 E2E 테스트라고 부를 수 있음
  • 통합테스트는 일부 의존성을 포함하여 진행
  • E2E테스트는 모든 의존성을 포함하기 때문에 통합테스트의 한 부분으로 볼 수 있음

 

 

 

테스트 구성

준비 given

실행 when

검증 then

 

준비

- 테스트 대상: System Under Test

- 협력자: SUT를 테스트하기 위해 필요한 요소
SUT의 메소드는 MUT

 

실행

필요한 만큼만 실행

 

검증

검증이 필요한 만큼만 검증

 

안티패턴

1. 단위테스트인 경우 여러 준비, 실행, 검증 사용하지 않기 --> 통합테스트

2. if 조건문으로 분기된 테스트

    테스트를 분리하는 것이 좋습니다.
    유사한 형태가 많다면, 매개변수화된 테스트를 사용해보는 것도 좋습니다.

3. 일반적인 비즈니스 로직에 대해 두 줄 이상으로 실행될 경우 

sut.purchase

sut.decreaseQuantity

설계부분 다시 한번 생각해 볼만합니다.

4. 테스트에 외부적인 요소로 인해 결과에 영향을 주지 않기

5. 외부의존성에 의해 고통받는 경우

 

 

예측할 수 있는 테스트, 에러 관련 테스트 다 작성해보는 것

동시성 문제 테스트 -> 의존성 문제가 있음 -> 통합적으로 실제 DB 활용할 수 있습니다.

 

 

 

 


 

 

TDD

 

TDD 흐름

--> 테스트 --> 코딩 --> 리팩토링 --> 

 

레드(실패) - 그린(성공) - 리팩터 로 부르기도 함

 

 

구현

쉬운 것부터 테스트

  • 구현하기 쉬운 테스트부터 시작하기
  • 예외 상황을 먼저 테스트 하기

 

 

테스트할 목록 정리

구현이 쉬운 테스트나 예외적인 테스트

 

테스트 과정에서 새로운 테스트 사례 발견시 그 사례를 목록에 추가해 놓치지 않도록 함

 

테스트 코드를 만들고 이를 통과시키고 리팩토링하고 다시 다음 테스트 코드를 만들고 통과시키는 과정 반복..

 

 

시작이 안 될 때는 단언부터 고민

검증하는 코드부터 작성하기

assertThat..

 

 

구현이 막히면

코드를 지우고 다시 시작

  • 쉬운 테스트 , 예외적인 테스트
  • 완급 조절

 

 

테스트 대역

테스트를 작성하다 보면 외부 요인이 필요한 시점이 있습니다. (File, DB, 결제, 외부 HTTP 등)

외부 요인은 테스트 작성을 어렵게 만들 뿐만 아니라 테스트 결과도 예측할 수 없게 만듭니다.

테스트 대상에서 의존하는 요인 때문에 테스트가 어려울 때는 대역(test double)을 써서 테스트를 진행할 수 있습니다.

 

방법 1

실제 PG 사용

    불필요한 결제 내역이 쌓일 수 있습니다. (지양)

 

방법 2

테스트 결제를 진행할 수 있는 테스트용 계정 활용

PG 사에서 의존

 

방법 3

테스트 대역 사용, 구현체 대신 인터페이스 사용하는 것이 좋습니다.

 

 

 

[stub, spy, mock, dummy, fake]

 

stub

    미리 준비해놓은 데이터를 가져오도록 작성

    내부 상호작용 

boolean existsByTitle() { return true; }

 

spy

    관리하지 못하는 의존성에 대한 응답 데이터를 반환하도록 작성

    응답 데이터를 직접 작성하여 기록한다고 생각하면 좋습니다.

    외부 상호작용, 호출의 관점

boolean sendTalk() {
    return true;
}

이후 실제로 해당 메소드를 따로 테스트 

 

mock

의존성에 대한 상호작용 검증

spy와 같이 관리할 수 없는 의존성에 대해서 사용

spy의 역할을 Mock을 사용해서 대역을 만들 수 있습니다.

 

dummy

SUT를 동작하게 하기 위한하드코딩 등의 값

 

Fake

인메모리로 저장하는 FakeRepository로 만들어 사용

Map<String, Object> map = new HashMap<>();

 

 


 

 

제어하기 힘든 외부 상황이 존재할 때, 의존을 도출하고 이를 대역으로 대신할 수 있다.

  • 제어하기 힘든 외부 상황을 별도 타입으로 분리
  • 테스트 코드는 별도로 분리한 타입의 대역을 생성
  • 생성한 대역을 테스트 대상의 생성자 등을 이용해서 전달
  • 대역을 이용해서 상황 구성

ex) StubCardValidator , Spy...

 


 

 

테스트 가능한 설계

  • 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
public void setFilePath(String filePath)...

 

  • 생성자나 세터를 통해 의존 대상 주입 받기
public PayService(PayDao payDao)

public void setPayDao(PayDao payDao)
  • 시간이나 임의 값 생성 기능 분리
class Times {

    public LocalDate today() {
        return LocalDate.now();
    }



class Loader {
    public int load() {
        LocalDate date = times.today();

 

외부 주입을 받는 것으로 분리 --> 테스트시 Mock 을 통해 임의의 시간대 / 임의 값으로 설정하여 테스트가 가능

given(times.today()).willReturn(LocalDate.of(2020.1.1));

 

  • 외부 라이브러리는 연동하기 위한 타입(클래스)를 따로 만듭니다.

XUtil이 있다면 해당 외부객체를 다루는 XService 타입을 만들어 비즈니스 로직 구현

해당 타입을 대역으로 테스트

 

 


 

 

HttpClient 와 같이 외부 API를 가져 올 때

 

WireMockServer 활용

JSON/XML 응답, HTTP지원, 단독 실행 등 다양한 기능을 제공하므로 외부 연동 코드를 테스트할 때 유용하게 사용할 수 있습니다.

 

RestTemplate 활용

스프링부트가 테스트 목적으로 제공하는 것으로서 내장 서버에 연결

 

 


 

 

테스트 코드와 유지보수

  • 변수나 필드를 사용해서 기댓값 표현하지 않기

assertEquals(1, .. ) 이런식으로 실제 값으로 표현

 

  • 두 개 이상을 검증하지 않기
  • 정확하게 일치하는 값으로 모의 객체 설정하지 않기

문자열일 경우 anyString(), Long 이면 anyLong() 등 범용적인 값을 사용

 

  • 과도하게 구현 검증하지 않기

내부 구현보다 실행 결과를 검증

 

  • 셋업에서 중복된 상황을 설정하지 않기
  • 실행 시점이 다르다고 실패하지 않기

public boolean isExpired() --> public boolean passedExpiryDate(LocalDateTime time) 

  • 랜덤하게 실패하지 않기
    • 랜덤 값을 생성하는 클래스로 분리하여 생성 가능
public Game(GameNumGen gen) {
	nums = gen.generate();
}

 

  • 필요하지 않은 값은 설정하지 않기
    • 동일 ID 검증하는 테스트에서 이름이나, 이메일 값은 필요하지 않습니다.
  • 단위테스트를 위한 객체 생성 보조 클래스
    • 팩토리 메서드 생성
  • 통합테스트는 필요하지 않은 범위까지 연동하지 않기
    • @SpringBootTest 는 컨트롤러, 서비스 등 모든 스프링 빈을 초기화, DB 관련 외에 나머지 설정도 처리
    • @JdbcTest 사용하면 DataSource, JdbcTemplate 등 DB 연동관련된 설정만 초기화

 

 


 

 

태깅과 필터링

Tag 애노테이션을 통해 테스트 포함 대상이나 제외 대상을 지정할 수 있습니다.

// build.gradle.kts
tasks.test {
    useJUnitPlatform {
        // 특정 태그가 있는 테스트만 실행
        includeTags("fast")

        // 특정 태그가 없는 테스트만 실행
        excludeTags("slow")

        // 복잡한 태그 표현식 사용
        includeTags("fast & !slow")
    }
}

 

 

  • @TempDir 으로 임시 폴더 생성
  • @Timeout(1) 실행시간 검증도 가능 
  • 임의 값 일치와 정확한 값 일치가 필요한 경우 eq() 메서드 사용
given(mockList.set(anyInt(), eq("123"))).willReturn("456");
String old = mockList.set(5,"123");
assertEquals("456", old);

 

 

 


 

 

JUnit5

JUnit5 주요 어노테이션

[@BeforeAll, @AfterAll, @BeforeEach, @AfterEach, @Nested]

 

  • @BeforeAll: 모든 테스트 메서드 실행 전에 한 번 실행되는 메서드 정의
  • @AfterAll: 모든 테스트 메서드 실행 후에 한 번 실행되는 메서드 정의
    • 테스트 환경 설정, 리소스 초기화/해제
  • @BeforeEach: 각 테스트 메서드 실행 전에 호출되는 메서드 정의
  • @AfterEach: 각 테스트 메서드 실행 후에 호출되는 메서드 정의
  • @Nested: 비정적 중첩 테스트 클래스 
    • @Nested 클래스로 비슷한 함수를 묶어서 사용할 수 있음
public class DisplayNameTest {

    @Nested
    class testA {

 

 

Mock

의존성에 대한 상호작용 검증할 때, 가짜 객체를 만들어 사용하는데 이것을 Mock이라고 합니다.

 

@Mock

  • Mockito 프레임워크에서 제공하는 어노테이션
  • Mock 객체를 생성할 때 사용

 

@InjectMocks

테스트 대상 클래스의 인스턴스를 생성하고, 해당 인스턴스에 @Mock 또는 @Spy로 선언된 의존성 객체를 자동으로 주입

이를 통해 테스트 코드를 간결하고 가독성있게 작성할 수 있습니다.

 

 

 

매개변수화된 테스트

    • 매개변수화된 테스트를 작성할 테스트 클래스를 생성합니다.
    • 테스트 메서드에 @ParameterizedTest 애노테이션을 적용합니다.
    • 매개변수 소스를 지정하는 애노테이션을 사용합니다. 예를 들어:
      • @ValueSource: 단일 값 배열 제공
      • @CsvSource: 쉼표로 구분된 값 제공
      • @MethodSource: 메서드에서 매개변수 제공
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.ValueSource;
    
    class MyServiceTest {
        @ParameterizedTest
        @ValueSource(strings = {"hello", "world", "junit5"})
        void testMyService(String input) {
            MyService service = new MyService();
            assertNotNull(service.processInput(input));
        }
    }
    
  1. 매개변수 소스 지정
    • 위의 예시에서는 @ValueSource를 사용하여 문자열 배열을 매개변수로 전달했습니다.
    • 다른 방법으로는 @CsvSource를 사용하여 쉼표로 구분된 값을 전달할 수 있습니다.
      @ParameterizedTest
      @CsvSource({"hello,true", "world,false", "junit5,true"})
      void testMyService(String input, boolean expected) {
          MyService service = new MyService();
          assertEquals(expected, service.processInput(input));
      }
      
    • @MethodSource를 사용하면 별도의 메서드에서 매개변수를 제공할 수 있습니다.
      @ParameterizedTest
      @MethodSource("provideStringsForTest")
      void testMyService(String input) {
          MyService service = new MyService();
          assertNotNull(service.processInput(input));
      }
      
      static Stream<String> provideStringsForTest() {
          return Stream.of("hello", "world", "junit5");
      }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

참고: 테스트 주도 개발 시작하기 - 최범균 저

댓글