Gom3rye 2025. 8. 20. 17:49
728x90
반응형

1.TDD(Test Driven Development): 테스트 주도 개발

1)개요

=>소프트웨어 개발 방법론 중 하나

=>기존에 테스트 코드의 작성은 프로덕션 코드가 작성된 이후에 이루어졌지만 TDD를 적용하면 프로덕션 코드보다 실패하는 테스트 코드를 먼저 작성하고 코드를 테스트를 통과하기 위해 최소한으로 개선한 후 테스트를 통화한 프로덕션 코드를 리팩토리하는 방식

=>TDD는 테스트만을 위한 기술이 아니며 오히려 소프트웨어 설계 방법론레 가까움

=>TDD는 하나의 테스트 기술이 아니고 분석 기술이며 설계 기술

 

2)TDD Cycle

=>RED -> GREEN -> REFACTOR

 

=>RED

- 테스트 코드 작성

- 동작하는 프로덕션 코드가 없는 상황에서 테스트 코드를 먼저 작성하는 것

- 앞으로 작성될 프로덕션 코드가 어떤 동작을 하면 좋을 지 작성

- 요구 사항을 작성하는 것 과 유사

- 테스트 코드를 먼저 작성하기 때문에 테스트 코드가 기대하는 로직이 실행되지 않아 테스트는 실패할 것

- 핵심은 실패하는 테스트 코드를 작성하는 것

- 2개의 정수를 더해서 결과를 리턴하는 작업을 수행해야 하는 경우

 

@Test

void plusTest(){

//given

Calculator calculator = new Calculator();

//when

int result = calculator.plus(1, 3);

//then

assertThat(result).isEqualTo(4);

}

 

=>GREEN

- 테스트를 통과하는 최소 코드 작성

- 실패하는 테스트 코드를 통과하도록 만드는 최소한의 코드를 작성

- 빠르게 구현할 수 있는 깔끔한 코드가 있다면 바로 작성하면 되지만 깔끔하게 코드를 작성하는 일이 오래 걸릴 것 같아면 일단 테스트 코드를 통과하는 데에만 집중하여 코드를 작성하는데 이를 죄악이라고 표현함

- GREEN을 보기 위해서는 명백한 실제 구현을 입력하는 방법도 있지만 최대한 빨리 GREEN을 보기 위해서는 상수를 반환하는 코드를 작성하고 점진적으로 변수로 바꾸어가는 방법도 있음

- 위의 코드를 테스트를 통과하도록 작성

public class Calculator{

public int plus(int a, int b){

return 4;

}

}

 

=>REFACTOR

- GREEN을 통과하기 위해 저지른 죄악을 수습

- 빠르게 GREEN을 보기 위해 작성한 코드를 좋은 코드로 리팩토링

public class Calculator{

public int plus(int a, int b){

return a+b;

}

}

 

3)TDD 원칙

=>실패하는 단위 테스트를 작성하기 전에는 프로덕션 코드를 작성하지 않음

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

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

 

4)TDD가 필요한 이유

=>변화에 대한 불안감 해소

- 프로덕션에 코드에 대한 테스트 코드가 이미 작성되어 있으므로 변화에 대한 영향력을 빠르게 파악할 수 있음

- 변경에 의한 문제를 사전에 빠르게 파악할 수 있음

- TDD의 핵심은 불안감 해소 와 용기

 

=>한번에 하나의 일만 집중할 수 있음

- TDD를 적용하지 않는 상황에서는 코드를 작성하다가 다른 코드에 한 눈이 팔리고 다른 모듈을 건드리게 되면 

- TDD를 적용하면 프로덕션 코드를 작성하며 현재 실패한 테스트 케이스를 통과하는데에만 집중할 수 있음

 

=>빠른 피드백

- TDD를 적용하면 작성한 테스트 코드로부터 빠른 주기로 피드백받고 리팩토링하며 점진적으로 지속적으로 코드를 개선할 수 있기 때문에 버그를 찾는 시점이 빨라질 수 있음

- 처음부터 완벽한 설계를 하는 것에 집중하는 것 보다는 설계를 점진적으로 개선해 나가는데 집중할 수 있음

 

=>테스트 코드 자체가 문서가 될 수 있음

 

=>테스트를 나중에 작성하는 것은 귀찮은 작업

- 테스트 코드를 나중에 작성하면 프로덕션 코드에 맞춰 통과하는 테스트 케이스를 만들 수 있음

 

=>코드 퀄리티가 높아짐

- TDD를 기반으로 테스트 코드를 작성하면 의존성이 높은 테스트 코드를 작성하기 어렵기 때문에 모듈간 결합도를 낮추고 응집도를 높일 수 있음

 

2.TEST의 종류

1)테스트 대상의 범위를 기준으로 한 종류

=>Unit Test(단위 테스트)

- 가장 작은 단위의 테스트

- 함수나 메서드 단위로 테스트를 수행하게 되며 함수나 메서드 호출을 통해서 의도한 결과가 나오는지 확인하는 수준으로 테스트를 진행

- 테스트 비용이 적게 들기 때문에 테스트 피드백을 빠르게 받을 수 있음

 

=>Integration Test(통합 테스트)

- 모듈을 통합하는 과정에서의 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행하는 테스트

- 단위 테스트는 모듈을 독립적으로 테스트하는 반면 통합 테스트는 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지를 확인

- 단위 테스트는 특정 모듈에 대한 테스트만 진행하기 때문에 데이터베이스나 네트워크 같은 외부 요인들을 제거하고 진행하는데 반해 통합 테스트는 외부 요인들을 포함하고 테스트를 진행하기 때문에 애플리케이션이 온전히 동작하는지를 테스트

- 테스트를 수행할 때 마다 모든 컴포넌트가 동작해야 하기 때문에 테스트 비용이 커지는 단점이 있음

 

=>System Test

- 시스템 테스트는 정보 시스템이 완전히 통합되어 구축된 상태에서 정보 시스템의 기능을 총체적으로 테스트 하는 것

- 통합된 각 모듈들이 원래 계획했던 대로 작동하는지 시스템의 실제 동작과 원래 의도했던 요구 사항과는 차이가 없는지 등을 판단

- 수행 시간, 파일 저장 및 처리 능력, 최대 부하, 복구 및 재시동 능력, 수작업 절차 등을 점검

- 시스템 테스트는 시스템의 내부적인 구현 방식이나 설계에 대한 지식에 관계없이 테스트를 수행하는 블랙박스 테스트의 일종

- 시스템 테스트는 모든 통합 테스트를 통과한 통합된 소프트웨어 컴포넌트들 그리고 필요한 하드웨어들과 통합된 소프트웨어 시스템 전체를 대상으로 수행

- 통합 테스트는 서로 통합된 두 소프트웨어 혹은 하드웨어 단위 사이에 서로 잘 맞지 않는 부분이 있는지를 파악하기 위해서 수행하고 시스템 테스트는 통합된 컴포넌트들 사이에 발생하는 결합과 통합된 전체 시스템내에서 발생하는 결함 모두를 발견하기 위해서 수행

 

=>Acceptance Test(인수 테스트)

- 프로젝트에 참여하는 사람들이 토의해서 시나리오를 만들고 개발자는 이에 의거해서 코드를 작성

- 개발자가 직접 시나리오를 제작할 수 도 있지만 다른 의사 소통 집단으로부터 시나리오를 받아 개발

- 인수 테스트는 애자일 개발 방법론에서 파생했는데 익스트림 프로그래밍(XP)에서 사용하는 용어로 시나리오가 정상적으로 동작하는지를 테스트하기 때문에 통합 테스트 와는 분류가 다름

- 익스트림 프로그래밍: 고객 요구 사항 변화에 빠르게 대응하고 고품질 소프트웨어를 빠르게 전달하기 위한 목적으로 프로그래밍 방식

실천 기법

TDD

Pair Programming: 두 명이 하나의 컴퓨터에서 코딩

CI

Refactoring

Collective Code Ownership: 공유 코드 소유

Iteration: 1~2 주 정도의 짧은 반복 주기

On Site Customer: 고객 상주

 

- 시나리오에서 요구하는 것은 누가 어떤 목적으로 무엇을 하는가 인데 개발을 하다보면 이런 기능은 API를 통해서 드러나게 되며 인수 테스트는 API를 확인하는 방식

- 고객이 명세한 요구사항 대로 잘 작동하는지 검증

- 실제 사용자 관점에서 테스트 하는 경우가 많기 때문에 인수 테스트는 소프트웨어 내부 코드에 관심을 가지지 않는 블랙박스 테스트

- Java에서는 RestAssured 나 MockMvc 같은 도구를 활용하여 인수 테스트 가능

 

2)Regression Test(회귀 테스트)

=>시스템을 운영 및 유지 보수하면서 발생한 버그들을 테스트 케이스로 만들어서 이전에 발생한 버그들이 다시 발생하지 않도록 테스트 하는 것

=>버그가 발생한 입력 조건 값을 사용하여 정상 동작하는 테스트 케이스를 작성

 

3)Black Box Test & White Box Test

=>Black Box Test

- 내부 구조나 소스 코드에는 관심을 두지 않고 Input 과 Output에 초점을 맞추는 방식

- 테스트 대상: 사용자가 보는 기능, UI, 요구사항 충족 여부

- 종류

Equivalence Partitioning: 모든 경우를 동일한 비율로 테스트

Boundary Value Analysis: 일반적으로 경계값에서 오류가 많이 발생하므로 경계값 앞 뒤의 데이터를 가지고 테스트

Decision Table

상태 전이 테스트

- 장점

개발 지식이 없어도 가능: 테스터나 사용자도 가능

요구사항 기준으로 테스트하므로 결함 발견이 사용자 경험에 가까움

- 단점

내부 로직을 보지 않기 때문에 코드 커버리지 보장이 어렵습니다.

테스트 케이스가 요구사항에만 의존

 

=>White Box Test

- 프로그램의 내부 구조, 소스코드 흐름을 고려해 테스트하는 방식

- 대상: 코드의 분기, 조건, 루프 등 구현 로직 자체

- 방법

Statement Coverage

Branch Coverage

Path Coverage

- 장점

내부 동작을 상세히 검증 - 버그 발견 확률이 높음

코드의 모든 경로를 테스트 - 테스트 커버리지가 높아짐

- 단점

개발 지식 필요 - 테스터가 코드를 이해해야 함

대규모 시스템에서는 모든 경로를 테스트하는 것이 어려움

 

4)Alpha Test 와 Beta Test

=>Alpha Test

- 개발자 또는 사용자(내부 테스터)가 개발사 내부에서 테스트

- 외부 사용자에게 배포하기 전에 치명적 결함을 제거하기 위해서 수행

- 기본 기능이 제대로 동작하는지 확인

 

=>Beta Test

- 실제 사용자에게 배포해서 사용성, 호환성, 버그를 검증하는 테스트

- 블랙박스 테스트 성격이 강함

- 목적

실제 사용자 관점에서 제품 평가

예상치 못한 환경적 이슈, 사용성 문제 해결

정식 추리 시 전 시장 반응 확인

 

5)Static Test(정적 테스트)

=>프로그램을 실행하지 않고 산출물(코드, 문서, 설계서 등)을 검토 분석하는 테스트 기법

=>소프트웨어 실행을 통한 동작 검증 과 달리 결함을 사전에 예방하는 목적이 강함

=>주요 활동

- Review

Peer Review

WalkThrough

Inspection

 

- Static Analysis

코드 실행없이 도구를 이용해서 코딩 규칙, 잠재적 버그, 보안 취약점을 자동 분석

SonarQube, ESLint, FindBugs, PMD 등

 

=>장점

- 소프트웨어 초기 단계에서 결함 발견 - 수정 비용 절감

- 실행 전 발견 가능 - 시간 과 자원 절약

- 품질 표준 및 코딩 규칙 준수 여부 확인 가능

- 문서/코드의 가독성, 유지보수성 향상

 

=>단점

- 실행 결과를 직접 확인하지 않음

- 모든 오류를 찾아낼 수 없음

- 리뷰 시 참여자의 역량에 좌우

 

3.테스트 코드 작성 방법

1)Given-When-Then 패턴

=>단계를 설정해서 각 단계의 목적에 맞게 코드를 작성

=>단계

- Given: 테스트를 수행하기 전에 테스트에 필요한 환경을 설정하는 단계로 테스트에 필요한 변수를 정의하거나 Mock 객체를 통해 특정 상황에 대한 행동을 정의하는 과정

 

- When: 테스트의 목적을 보여주는 단계로 실제 테스트 코드가 포함되며 테스트를 통한 결과를 가져오게 됨

 

- Then: 테스트의 결과를 검증하는 단계로 When 단계에서 나온 결과를 검증하는 작업을 수행하며 결과가 아니더라도 테스트를 통해서 검증해야 한다면 이 단계에 포함

=>단위 테스트보다는 인수 테스트에서 사용하는 것을 권장

 

=>코드 예시

 

@Test

public void testHashSetContainsNonDuplicatedValue(){

//Given

Integer value = Integer.valueOf(1);

Set<Integer> set = new HashSet<>();

 

//When

set.add(value);

set.add(value);

set.add(value);

 

//Then

Assertion.assertEquals(1, set.size());

Assertion.assertTrue(set.contains(value));

 

}

 

2)좋은 테스트를 위한 5가지 속성

=>Fast

- 테스트 코드는 빠르게 동작해야 함

 

=>Isolation

- 하나의 테스트 코드는 목적으로 여기는 하나의 대상에 대해서 수행되어야 한다.

 

=>Repeatable

- 반복 가능

 

=>SelfValidating

- 테스트는 그 자체로 테스트의 검증이 완료되어야 함

 

=>Timely

- 테스트 코드는 테스트하려는 애플리케이션 코드를 구현하기 전에 완성되어 함

 

 

4.Spring Boot 테스트 설정

=>별도의 starter를 제공: spring-boot-starter-test

1)제공되는 라이브러리

- JUnit

테스트 케이스를 작성하고 실행할 수 있는 기능들을 제공

테스트 케이스를 작성할 수 있는 어노테이션 과 실행한 테스트 결과 값을 예상 값과 비교 및 검증할 수 있는 클래스들을 제공

 

- Hamcrest

실행한 테스트 결과 값을 예상 값과 비교 및 검증할 수 있는 클래스들을 제공

JUnit 보다는 가독성이 높음

여기에 있는 메서드들은 서술적으로 작성할 수 있기 때문 

 

- Mockito

개발자가 입력 값에 따라 출력 값을 프로그래밍 한 일종의 가짜 객체

목 객체를 사용할 수 있는 기능을 제공

 

- spring-test

스프링 프레임워크의 기능을 통합 테스트 할 수 있는 기능을 제공

 

- spring-boot-test

스프링 부트 프레임워크의 기능을 통합 테스트 할 수 있는 기능을 제공

 

- sprint-boot-test-autoconfiguration

스프링 부트 프레임워크의 통합 테스트를 자동화 해주는 기능을 제공

 

2)JUint을 테스트

=>개요

- 자바 언어에서 사용되는 대표적인 테스트 프레임워크로서 단위 테스트 뿐 만 아니라 통합 테스트를 할 수 있는 기능을 제공

- Annotation 기반의 테스트 방식을 지원하는데 몇 개의 Annotation 만으로 간단하게 테스트 코드를 작성

 

=>@Test

- 테스트 코드를 포함하는 메서드를 정의할 때 사용하는 Annotation

- 메서드의 접근 지정자는 public

- 리턴 타입은 void

- 메서드 이름은 test로 시작하는 것이 일반적

 

=>JUnit life cycle

- @BeforeAll: 테스트 전에 한 번 만 수행

- @BeforeEach: 테스트 메서드 각각에 실행

- @AfterAll: 테스트 후에 한 번 만 수행

- @AfterEach: 테스트 메서드 개수만큼 실행

 

=>테스트 코드 작성

import org.junit.jupiter.api.*;

 

import java.util.HashSet;

import java.util.Set;

 

public class MiscTest {

 

    @BeforeAll

    public static void setUp(){

        System.out.println("setUp");

    }

 

    @BeforeEach

    public void init(){

        System.out.println("init");

    }

 

    @AfterAll

    public static void destroy(){

        System.out.println("destroy");

    }

 

    @AfterEach

    public  void cleanUp(){

        System.out.println("cleanUp");

    }

 

    @Test

    public void testHashSetContainsNonDuplicatedValue(){

        //Given

        Integer value = 1;

        Set<Integer> set = new HashSet<>();

 

        //When

        set.add(value);

        set.add(value);

        set.add(value);

 

        //Then

        Assertions.assertEquals(1, set.size());

        Assertions.assertTrue(set.contains(value));

 

    }

 

    @Test

    public void testDummy(){

        Assertions.assertTrue(true);

    }

}

 

=>Assertions 클래스

- static 검증 메서드를 제공

- 검증 메서드는 assert 라는 머리말로 시작

- 검증에 실패하는 경우 AssertionFailedError 예외가 발생하고 JUnit 프레임워크는 이 정보를 바탕으로 각 테스트 케이스의 성공 과 실패 여부를 판단

- 검증 메서드의 인자 이름이 expect 이면 예상값을 의미하고 actual 이면 테스트 대상의 실제 값을 의미

- 2개의 인자를 받는 메서드의 인자 순서는 변경이 되도 테스트를 진행할 수 있지만 순서가 변경이되면 정확한 테스트 결과 메시지를 볼 수 없음

 

3)@SpringBootTest

=>이 어노테이션을 클래스에 추가하면 이 클래스는 Spring Boot의 Annotation을 인지하게 됩니다.(ApplicationContext 사용이 가능)

 

=>내장 컨테이너를 이용해서 테스트

- 요청 DTO 클래스 생성: dto.HotelRequest

@Getter

@Setter

@ToString

public class HotelRequest {

    private String hotelName;

    

    public HotelRequest(){

        

    }

    public HotelRequest(String hotelName) {

        this.hotelName = hotelName;

    }

}

 

- 응답 DTO 클래스 생성 - dto.HotelResponse

import lombok.Getter;

import lombok.Setter;

import lombok.ToString;

 

@Getter

@Setter

@ToString

public class HotelResponse {

    private Long hotelId;

    private String hotelName;

    private String address;

    private String phoneNumber;

 

    public HotelResponse(Long hotelId, String hotelName, String address, String phoneNumber) {

        this.hotelId = hotelId;

        this.hotelName = hotelName;

        this.address = address;

        this.phoneNumber = phoneNumber;

    }

}

 

=>Service 인터페이스 생성 - service.DisplayService

import com.example.demo.dto.HotelRequest;

import com.example.demo.dto.HotelResponse;

 

import java.util.List;

 

public interface DisplayService {

    List<HotelResponse> getHotelsByName(HotelRequest hotelRequest);

}

 

=>Service 클래스 생성 - service.DisplayServiceImpl

import com.example.demo.dto.HotelRequest;

import com.example.demo.dto.HotelResponse;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Service;

 

import java.util.List;

import java.util.concurrent.TimeUnit;

 

@Slf4j

@Service

public class DisplayServiceImpl implements DisplayService{

 

 

    public List<HotelResponse> getHotelsByName(HotelRequest hotelRequest) {

        try{

            TimeUnit.SECONDS.sleep(5);

        }catch(InterruptedException e){

            log.error("ERROR", e);

        }

 

        return List.of(new HotelResponse(

                1000L, "Ragged Point Inn", "18091 CA -1",

                "01037901997"));

    }

}

 

=>Test를 위한 클래스를 생성: DisplayServiceTest

 

import com.example.demo.dto.HotelRequest;

import com.example.demo.dto.HotelResponse;

import com.example.demo.service.DisplayService;

import com.example.demo.service.DisplayServiceImpl;

import org.junit.jupiter.api.Assertions;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.context.ApplicationContext;

 

import java.util.List;

 

 

@SpringBootTest

public class DisplayServiceTest {

    @Autowired

    private DisplayService displayService;

 

    @Autowired

    private ApplicationContext applicationContext;

 

    @Test

    public void testDisplayService(){

        //Given

        HotelRequest hotelRequest = new HotelRequest("Line Hotel");

 

        //When

        List<HotelResponse> hotelResponses = displayService.getHotelsByName(hotelRequest);

 

        //Then

        Assertions.assertNotNull(hotelResponses);

        Assertions.assertEquals(1,hotelResponses.size());

    }

 

    @Test

    public void testApplicationContext(){

        DisplayService displayService =

                applicationContext.getBean(DisplayService.class);

        Assertions.assertNotNull(displayService);

        Assertions.assertTrue(DisplayService.class.isInstance(displayService));

 

    }

 

}

 

=>build.gradle 파일의 dependencies 부분에 추가

    compileOnly 'org.projectlombok:lombok:1.18.34'

    annotationProcessor 'org.projectlombok:lombok:1.18.34'

 

4)TestConfiguration

=>테스트 케이스를 작성할 때는 테스트 대상 클래스의 기능을 테스트 할 수 있도록 테스트 범위를 줄이는 것이 중요 

=>대상 클래스의 기능이 다른 클래스에 의존할 때는 의존하는 클래스 설정에 따라 대상 클래스의 기능을 정확하게 테스트할 수 없는 경우가 발생

=>테스트 대상 클래스 내부에 데이터베이스에 쿼리하는 기능을 포함하거나 원격 서버의 RES-API를 호출하는 기능을 포함하는 경우 테스트 환경은 Dev 또는 Production 환경 과 분리되어 있어 테스트를 위한 데이터베이스나 서버를 구축해야 하는데 서버를 구축해도 같은 테스트를 여러번 실행하면 데이터베이스의 데이터나 서버의 데이터가 훼손되서 매번 같은 결과를 얻을 수 없을 수 있음 

=>테스트 환경을 구축하지 못했다면 테스트를 실행할 때마다 ConnectionException 같은 예외가 발생하여 테스트를 정상적으로 실행할 수 없으므로 테스트를 할 때 테스트 대상 클래스의 기능만 독립적으로 테스트 할 수 있으면 되는데 이 경우 가상의 실행 환경을 만들면 가능

=>스프링 프레임워크에서 제공하는 @TestConfiguration 어노테이션을 사용하면 테스트 환경을 쉽게 구축할 수 있음

=>@TestConfiguration 은 @Configuration과 비슷한 기능을 제공하며 자바 설정 클래스를 정의하는 목적으로 사용

=>@TestConfiguration이 사용된 클래스에는 @Bean 으로 정의한 스프링 빈을 포함할 수 있음

=>Test 환경 설정

- Entity 클래스 생성: domain.HotelRoomEntity

 

import lombok.Getter;

import lombok.Setter;

import lombok.ToString;

 

@Getter

@Setter

@ToString

public class HotelRoomEntity {

    private Long id;

    private String code;

    private Integer floor;

    private Integer bedCount;

    private Integer bathCount;

 

    public HotelRoomEntity(Long id, String code, Integer floor, Integer bedCount, Integer bathCount) {

        this.id = id;

        this.code = code;

        this.floor = floor;

        this.bedCount = bedCount;

        this.bathCount = bathCount;

    }

 

}

 

- 응답하는 클래스 - domain.HotelRoomResponse

 

import lombok.Data;

 

@Data

public class HotelRoomResponse {

    private Long id;

    private String code;

    private Integer floor;

    private Integer bedCount;

    private Integer bathCount;

    

    public HotelRoomResponse(Long id, String code, Integer floor, Integer bedCount, Integer bathCount) {

        this.id = id;

        this.code = code;

        this.floor = floor;

        this.bedCount = bedCount;

        this.bathCount = bathCount;

    }

 

   //Entity를 받아서 Response로 변경해주는 메서드

    public static HotelRoomResponse from(HotelRoomEntity hotelRoomEntity){

        return new HotelRoomResponse(hotelRoomEntity.getId(),

                hotelRoomEntity.getCode(),

                hotelRoomEntity.getFloor(),

                hotelRoomEntity.getBedCount(),

                hotelRoomEntity.getBathCount());

    }

}

 

- 데이터베이스 연동 클래스: persistence.HotelRoomRepository

@Repository

public class HotelRoomRepository {

    public HotelRoomEntity findById(Long id) {

        return new HotelRoomEntity(id, "EAST-1902", 19, 2, 1);

    }

}

 

- 서비스 클래스 생성: service.HotelRoomDisplayService

 

import com.example.demo.domain.HotelRoomEntity;

import com.example.demo.domain.HotelRoomResponse;

import com.example.demo.persistence.HotelRoomRepository;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import org.springframework.stereotype.Service;

 

@Slf4j

@Service

//@RequiredArgsConstructor

public class HotelRoomDisplayService {

    private final HotelRoomRepository hotelRoomRepository;

    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;

    

    public HotelRoomDisplayService(HotelRoomRepository hotelRoomRepository, 

                                   ThreadPoolTaskExecutor threadPoolTaskExecutor) {

        this.hotelRoomRepository = hotelRoomRepository;

        this.threadPoolTaskExecutor = threadPoolTaskExecutor;

    }

    

    public HotelRoomResponse getHotelRoomById(Long id){

        HotelRoomEntity hotelRoomEntity = hotelRoomRepository.findById(id);

        threadPoolTaskExecutor.execute(() -> {log.warn("entity:{}", hotelRoomEntity.toString());});

        return  HotelRoomResponse.from(hotelRoomEntity);

    }

    

}

 

- Configuration 클래스 생성: config.ThreadPoolConfig

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

 

@Configuration

public class ThreadPoolConfig {

    @Bean(destroyMethod="shutdown")

    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setMaxPoolSize(10);

        executor.setThreadNamePrefix("AsyncExecutor-");

        executor.afterPropertiesSet();

        return executor;

    }

}

 

- Test를 위한 Configuration 클래스 생성 - config.TestConfig

import org.springframework.boot.test.context.TestConfiguration;

import org.springframework.context.annotation.Bean;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

 

@TestConfiguration

public class TestConfig {

    @Bean(destroyMethod="shutdown")

    public ThreadPoolTaskExecutor threadPoolTaskExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setMaxPoolSize(1);

        executor.setThreadNamePrefix("TestExecutor-");

        executor.afterPropertiesSet();

        return executor;

    }

}

 

- 2개의 설정 클래스에서 동일한 Bean을 2개 만들려고 하므로 Overriding 할 수 있도록 설정을 추가하기 위해서 resources 디렉토리에 설정 파일을 추가하고 작성: application-test.properties

 

spring.main.allow-bean-definition-overriding=true

 

- Test 클래스 생성: HotelRoomDisplayServiceTest

import com.example.demo.config.TestConfig;

import com.example.demo.domain.HotelRoomResponse;

import com.example.demo.service.HotelRoomDisplayService;

import org.junit.jupiter.api.Assertions;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.ContextConfiguration;

import org.springframework.test.context.TestPropertySource;

 

@SpringBootTest

@ContextConfiguration(classes = TestConfig.class)

@TestPropertySource(locations = "classpath:application-test.properties")

public class HotelRoomDisplayServiceTest {

    @Autowired

    private HotelRoomDisplayService hotelRoomDisplayService;

 

    @Test

    public void testTestConfiguration(){

        HotelRoomResponse hotelRoomResponse = hotelRoomDisplayService.getHotelRoomById(1L);

        Assertions.assertNotNull(hotelRoomResponse);

        Assertions.assertEquals(1L, hotelRoomResponse.getId());

    }

}

 

5)MockBean

=>스프링 부트의 기본 테스트 모듈에는 Mockito 와 @MockBean 어노테이션을 같이 제공

=>가짜 객체를 만들 수 있는 라이브러리

=>가짜 Repository를 이용하는 테스트: HotelRoomDisplayServiceTest

import com.example.demo.domain.HotelRoomEntity;

import com.example.demo.domain.HotelRoomResponse;

import com.example.demo.persistence.HotelRoomRepository;

import com.example.demo.service.HotelRoomDisplayService;

import org.junit.jupiter.api.Assertions;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

 

 

import org.springframework.test.context.bean.override.mockito.MockitoBean;

 

import static org.mockito.ArgumentMatchers.any;

import static org.mockito.BDDMockito.given;

 

@SpringBootTest

public class HotelRoomDisplayServiceTest01 {

    @Autowired

    private HotelRoomDisplayService hotelRoomDisplayService;

 

    //가짜 객체 생성

    @MockitoBean

    private HotelRoomRepository hotelRoomRepository;

 

    @Test

    public void testMockBean(){

        given(this.hotelRoomRepository.findById(any()))

                .willReturn(new HotelRoomEntity(10L, "test", 1,1,1));

        HotelRoomResponse hotelRoomResponse = hotelRoomDisplayService.getHotelRoomById(1L);

 

        Assertions.assertNotNull(hotelRoomResponse);

        Assertions.assertEquals(10L, hotelRoomResponse.getId());

    }

}

 

6)Test Slice Annotation

=>개요

- @SpringBootTest 어노테이션으로 테스트를 실행하면 ApplicationContext를 이용하여 스프링 빈을 스캔하고 의존성을 주입

- 애플리케이션의 기능이 많다면 스캔해야 할 대상이 많아지고 그 만큼 많은 객체를 생성해야하기 때문에 테스트 시간이 오래 걸릴 수 밖에 없는데 이는 F.I.R.S.T 원칙 중 Fast 항목에 위배됨

- 테스트를 진행하여 결과를 확인하는 피드백 시간이 늘어나면 애플리케이션을 디버깅 할 시간이 줄어들고 배포 전략에 따라 테스트를 통과해야 패키징을 할텐데 테스트 시간이 오래 걸리면 빠른 배포를 할 수 없음

- 스프링 부트 테스트 프레임워크에서는 테스트 시간을 줄이기 위해서 테스트 슬라이스 개념을 제공하는데 기능 별로 잘라서 테스트 대상을 줄일 수 있는 방법을 제공

- 테스트 대상이 작아지고 ApplicationContext가 스캔해야 하는 스프링 빈의 개수도 줄어들고 기능을 초기화하지 않아도 됨

 

=>종류

- @WebMvcTest

스프링 MVC 프레임워크의 기능을 테스트하는 것으로 @Controller, @ControllerAdvice를 스캔하고 Converter, Filter, WebMvcConfigurer 같은 기능을 제공

@Service, @Component, @Repository는 스캔하지 않음

 

- @DataJpaTest

JPA 기능을 테스트

@Repository 만 스캔

 

- @JsonTest

직렬화 관련 기능만 테스트

 

- @RestClientTest

클라이언트의 동작을 테스트

 

- @DataMongoTest

 

=>Controller 클래스 테스트

- Controller 클래스 생성 - HotelController

 

import com.example.demo.dto.HotelRequest;

import com.example.demo.dto.HotelResponse;

import com.example.demo.service.DisplayService;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.ResponseBody;

import org.springframework.web.bind.annotation.RestController;

import java.util.List;

 

@Slf4j

@RestController

@RequiredArgsConstructor

public class HotelController {

    private final DisplayService displayService;

 

    @ResponseBody

    @PostMapping("/hotels/fetch-by-name")

    public ResponseEntity<List<HotelResponse>> getHotelByName(@RequestBody HotelRequest hotelRequest){

        List<HotelResponse> hotelResponseList = displayService.getHotelsByName(hotelRequest);

        return ResponseEntity.ok(hotelResponseList);

    }

}

 

- Json을 위한 클래스 생성 - JsonUtil

import com.fasterxml.jackson.databind.ObjectMapper;

 

public class JsonUtil {

    public static final ObjectMapper mapper = new ObjectMapper();

}

 

- Test 클래스 생성: ApiControllerTest01

 

728x90
반응형