Skip to main content

Command Palette

Search for a command to run...

Tdd(테스트 기반 개발) / 계층별 테스트 구현

Published
16 min read

TDD

🧪 TDD 방법론 (테스트 주도 개발) - 알기 쉽게 정리

TDD(Test Driven Development) 란 ‘테스트 주도 개발’ 로서 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.

중요한 것은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해야 하는 것이다.

이를 통해, 실제 코드에 대해 기대되는 바를 보다 명확하게 정의함으로써 불필요한 설계를 피할 수 있고, 정확한 요구 사항에 집중할 수 있다.

즉, 간단히 말하자면 통해 발생할, 동작할 모든 상황에 대한 것을 테스트 코드로 구현 한 뒤, 실제 코들를 작성하고 테스트 하는 것이다.

이렇게 되면 테스트 코드를 작성하면서 기능에 대한 명확한 설계도 가능해진다.

처음에는 어색하지만 숙련된다면

  • 디버깅 시간 단축

  • 테스트 코드가 이미 있기에 빠른 피드백이 가능하다

  • 안정성이 높아져 생산성이 높아진다.

  • 재설계 시간이 단축된다.

  • 추가 구현이 용이하다.

하지만 숙련되지 않을 때는 오히려 생산성이 저하되고, 아마 이전 개발 방식과 달라 전체적으로 버벅거릴 확률이 높아진다. 그리고 정해진 체계안에서 해야되기에 다소 경직되는 경우가 있다.

테스트 코드 요소

[Spring] JUnit & Mockito 기반 Spring 단위 테스트 코드 작성

TDD 는 작은 기능 별로 테스트 코드를 작성 후 그것을 기반으로 기능을 구현하는 것이기 때문에 단위 테스트를 구현해야한다.

단위 테스트

  • 단위테스트는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트다.

  • 하나의 모듈이란 각 계층에서의 하나의 기능 또는 메소드로 이해할 수 있다.

  • 하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것이다.

당연히 작은 단위에 하나의 기능 혹은, 메소드 단위의 테스트이기 때문에 전체 프로그램의 대한 테스트는 별개의 테스트(통합 테스트)로 진행하여야하고, 당연하지만 정말 모든 것을 테스트할 수 있는 코드를 짜는 것은 굉장히 힘들기 때문에 어느정도 타협해야 할 수도 있다.

테스트 코드 작성 공통 준수 사항

  • 보통 테스트를 위한 라이브러리로 JUnit과 AssertJ 조합을 사용하여 테스트를 한다.

    JUnit과 JAssert를 같이 사용하는 것이 일반적인 방법이다.

    • JUnit은 테스트 케이스를 작성하고 실행하는 라이브러리

    • JAssert는 테스트 중에 예상 결과를 확인하고 테스트의 성공 또는 실패를 판단하는 데 사용하는 라이브러 어설션(assertion)은 테스트 케이스 내에서 예상되는 조건을 검사하는 데 사용되는 도구로서 그 대표적인 것이 JAssert 라이브러리

  • Given/When/Then 패턴

    • Given : 어떠한 데이터가 주어질 때.

    • When : 어떠한 기능을 실행하면.

    • Then : 어떠한 결과를 기대한다.

@Test
@DisplayName("Test")
void test() {
    // Given

    // When

    // Then
}

Mockito(= Mock Objects = 모의 객체)를 사용한 단위 테스트

Mockito는 Java 언어를 위한 모의 객체(Mock Objects) 프레임워크 중 하나로, 주로 단위 테스트(Unit Testing) 시에 다른 객체와의 협력을 시뮬레이션하거나 대체하기 위해 사용된다.

단위 테스트를 작성할 때, 특히 의존하는 다른 객체가 있을 때, 해당 객체들이 원활하게 동작하는 것을 가정하고 실제 객체 대신 가짜(Mock) 객체를 사용하여 테스트하는 것이 일반적이다. Mockito를 사용하면 이러한 가짜 객체를 만들고, 그 객체의 동작을 제어하고 예상대로 동작하도록 가짜 객체를 구성할 수 있다.

Service, Controller, Repository 등등 Spring 웹 프로젝트의 경우 각 클래스(객체)들이 빈에 등록되어 Spring에 관리되며 서로에게 의존성이 부여되기에 하나를 시험하기 위해서는 다른 모든 것을 일일이 값을 넣어 객체 만들고 설정한 후에 동작시켜야하는 경우가 생긴다. 이러면 생각보다 거대한 작업이 되기도 하고 신경써야 되는 점도 많으니, 가짜 객체를 쓰는 것이다.

예를 들어 Controller Test를 하기 위해 Service의 함수가 필요해 Service 객체를 만들었다고 치자, 이는 Spring Bean에 등록되어지는 객체가 아닌 독립된 순수 객체이므로 Spring이 알아서 Repository와 연결해 처리해주던 작업이 동작하지 않게 된다. 그것에 대한 처리(오버라이드, 값부여 …)를 일일이 해주어야 하기에 Mock 객체를 쓰는 것이다.

모의 객체(Mock Objects)를 사용하는 이점은 다음과 같다

  1. 의존성 관리: 의존하는 객체가 변경되거나 오류가 발생할 경우, 모의 객체를 사용하여 테스트를 실행할 수 있으므로, 테스트가 안정적으로 유지된다.

  2. 격리된 테스트: 모의 객체를 사용하면 특정 기능에 집중할 수 있으며, 다른 객체들의 실제 동작에 영향을 받지 않고 테스트할 수 있다.

  3. 테스트 가능한 코드 작성: 모의 객체를 사용하면 테스트를 위해 협력하는 객체의 동작을 미리 정의하고 테스트에 활용할 수 있으므로, 테스트 가능한 코드를 작성하기 용이합니다.

이제 위에서 설명한 JUnit, JAssert 라이브러리와 Mockito를 활용하여 테스트코드를 구현하는 방법을 알아보자.

테스트 클래스

주의 사항 (의존성 주입)

Controller 클래스에서 @Service 붙여 만든 서비스 클래스의 객체를 만들 때 별도로 설정할 것이 없다. 지금 클래스가 컨트롤러 클래스고 그 안에 서비스 필드 객체를 만드는 사실을 Spring이 인지하고 Bean등록 및 의존성 주입을 알아서 해주기 때문이다.

@Controller
public class MyController {
        // Service 객체 생성
    private final MyService myService;

}

하지만 웹 에플리케이션 동작부가 아닌 후에 정리할

  • @SpringBootTest

  • @WebMvcTest

  • @ExtendWith

  • @DataJpaTest

이와 같은 어노테이션이 붙은 클래스의 경우 테스트 클래스로서 웹 에플리케이션를 동작시키지 않기 때문에 Spring이 동작하지 않아 앞서 본 것처럼 각 기능 클래스에 대한 Bean 등록, 의존성 부여를 해주지 않기 때문에 직접 처리해주어야 한다.

아래 Controller Test 클래스 예제로 확인해보자

@ExtendWith(SpringExtension.class)
OR
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
OR
...
public class MyNonWebTest {

        // 직접 의존성 주입 설정 
    @Autowired
    private MyService myService;

    @Test
    public void testSomething() {
        // 테스트 코드 작성
    }
}

Test 어노테이션

해당 클래스를 Test 클래스로 지정해주는 어노테이션들을 알아보자, 각각 담당하는 부분과 기능이 다르다.

  1. @SpringBootTest:

    • 기능: **@SpringBootTest**는 스프링 부트 애플리케이션의 통합 테스트를 위해 사용된다. 이 어노테이션을 사용하면 실제로 애플리케이션 컨텍스트를 로드하고, 모든 빈을 등록하며, 실제 웹 서버(예: Tomcat)를 시작하고 가상 웹 환경을 설정한다. 즉, 실제로 애플리케이션을 실행하며 테스트를 수행한다. 단!!! 실제 웹서버가 아닌 가상 웹서버여서 빈이 등록은 되지만 의존성 주입은 @Autowired를 통해 따로 해주어야 한다.

    • 장점:

      • 실제 애플리케이션과 유사한 환경에서 테스트를 수행할 수 있어 실제 동작을 확인할 수 있다.

      • 통합 테스트를 통해 여러 컴포넌트 간의 상호작용을 테스트할 수 있다.

    • 단점:

      • 실행 시간이 오래 걸릴 수 있어 빠른 피드백을 얻기 어려울 수 있다.

      • 테스트 간 의존성 문제와 상태 관리가 복잡할 수 있다.

    • 주로 쓰이는 대상: 실제 애플리케이션의 전체 기능을 테스트하고자 할 때 사용한다.

  2. @WebMvcTest:

    • 기능: **@WebMvcTest**는 웹 애플리케이션의 MVC 계층을 테스트하기 위해 사용됩니다. 주로 웹 컨트롤러(Controller)와 관련된 빈들만을 로드하고 테스트 환경을 설정합니다.

    • 장점:

      • 웹 계층의 컨트롤러 테스트에 특화되어 있어 실행 속도가 빠릅니다.

      • Web Layer에 해당하는 빈만 빠르게 생성되어 테스트할 수 있다.

        • @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, HandlerMethodArgumentResolver 빈들이 스캔되어 등록

        • @Component와 @ConfigurationProperties는 스캔되지 않는다. 따로 import 하던지 해서 등록 필요

      • 컨트롤러와 관련된 테스트 시나리오를 효과적으로 수행할 수 있습니다.

    • 단점:

      • 웹 계층 외의 다른 빈들에 대한 테스트가 불가능합니다.

      • 웹 계층과 연관된 빈들만 주입받으므로 컨트롤러와 서비스 계층 사이의 상호작용을 테스트하기 어려울 수 있습니다.

    • 주로 쓰이는 대상: 웹 컨트롤러(Controller) 테스트 및 웹 계층 관련 로직 테스트에 사용합니다.

  3. @ExtendWith:

    • 기능: **@ExtendWith**는 JUnit 5에서 사용되며, 테스트 실행 환경을 확장할 때 사용됩니다. 스프링 부트에서는 **@ExtendWith(SpringExtension.class)**을 사용하여 스프링 관련 확장을 적용할 수 있습니다.

    • 장점:

      • JUnit 5의 기능을 활용하여 다양한 확장(extension)을 적용할 수 있습니다.

      • 테스트 실행 환경을 커스터마이징할 수 있습니다.

    • 단점:

      • @ExtendWith 자체로는 스프링 부트의 특정 환경을 설정하지 않으므로 추가적인 설정이 필요합니다.
    • 주로 쓰이는 대상: JUnit 5와 함께 사용하여 테스트 환경을 커스터마이징하거나 확장할 때 사용합니다.

  4. @DataJpaTest:

    • 기능: **@DataJpaTest**는 JPA(Java Persistence API) 리포지토리와 관련된 테스트를 위해 사용됩니다. 이 어노테이션을 사용하면 JPA 리포지토리를 테스트하기 위한 데이터베이스 설정과 관련된 빈들만을 로드하고 테스트 환경을 설정합니다.

    • 장점:

      • JPA 리포지토리와 관련된 데이터베이스 테스트를 빠르게 실행할 수 있습니다.

      • 내장 데이터베이스를 사용하여 테스트를 수행하므로 별도의 데이터베이스 설정이 필요하지 않습니다.

    • 단점:

      • JPA 리포지토리 이외의 다른 빈들에 대한 테스트가 불가능합니다.

      • 실제 데이터베이스를 사용하지 않으므로 일부 데이터베이스 관련 이슈를 감지하지 못할 수 있습니다.

    • 주로 쓰이는 대상: JPA 리포지토리와 관련된 데이터베이스 테스트에 사용합니다.

간단정리

위처럼 각 테스트 방식, 도구들의 특징으로 인해 절대적이진 않지만 웹 프로젝트 각 계층별로 보편적으로 쓰는 테스트 방식이 있게 되는 것이다.

  • 웹 어플리케이션 통합 테스트 : @SpringBootTest

  • Controller Test : @WebMvcTest

  • Service Test : @ExtendWith

  • Repository Test : @DataJpaTest

검증 함수 assertThat

앞서 말한 내용은 테스트 방식 종류에 대한 설명이었고, 여러 조건을 고려해 특정 구조로 테스트 클래스를 만들고, 그안에 @Test 어노테이션이 붙은 테스트 메서드를 만든다.

그 테스트 메서드에서 실제 테스트를 위해 동작과 결과가 올바른지 확인하는 검증함수인 assertThat 함수를 사용해야한다.

assertThat함수

[Spring] 테스트 코드 - JUnit, AssertJ

테스트 코드에서 특정 조건들이 참인지 검증하기 위해 메서드 체이닝(Method Chaining)을 통해 여러 조건들을 받아 확인하는 assertThat 함수를 알아보자

spring-boot-starter-test 목록

  • JUnit: 자바용 단위 테스트 프레임워크

  • Spring Test & Spring Boot Test: 스프링 부트 애플리케이션을 위한 통합 테스트 지원

  • AssertJ: 검증문인 Assertion을 작성하는데 사용되는 라이브러리

  • Hamcrest: 표현식을 이해하기 쉽게 만드는데 Matcher 라이브러리

  • Mockito: 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리, 검증할 수 있게 지원하는 테스트 프레임워크

  • JSONassert: JSON용 Assertion 라이브러리

  • JsonPath: JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리

JUnit, AssertJ 라이브러리는 각각 assertThat 을 가지고 있다.

그리고 JUnit, AssertJ 라이브러리는 보통 스프링 웹 프로젝트 생성 시 같이 포함되는 라이브러리 spring-boot-starter-test 안에 들어있으니, 별다른 라이브러리 추가 없이 테스트 코드를 바로 작성 가능하다.

Junit 자체도 테스트 메서드에서 예상결과를 검증하는 Assertion 메서드를 제공하긴 하지만, 사람들은 더 다양한 Assertion 기능을 제공하는 AssertJ을 같이 쓴다.

AssertJ는 Junit5에서 제공하는 Assertion method 이외의 추가적인 메서드를 지원하며, 메서드 체이닝을 지원하여 더 직관적이고 읽기 쉬운 테스트코드를 작성가능하게 해주고, 더 정확한 에러 메세지를 제공한다.

Junit 의 Assertion

Assertions.assertThat(sum, a+b); // 첫번째 인수에는 기대값, 두번째에는 검증하는 값

AssertJ 의 Assertion

assertThat(sum).isEqualTo(a+b);

매개변수를 받는 스타일이 달라 가독성도 좋아진다.

모든 테스트 코드는 assertThat() 메소드에서 출발한다.

다음과 같은 포맷으로 AssertJ에서 제공하는 다양한 메소드를 연쇄 호출 하면서 코드를 작성할 수도 있다.

assertThat(테스트 타켓).메소드1().메소드2().메소드3();

AssertJ 관련 메서드

org.assertj.core.api package summary - assertj-core 3.24.2 javadoc

img1.daumcdn.png

테스트 코드 구현

앞의 내용을 정리하자면

  • JUnit과 JAssert 라이브러리를 이용(기본에 포함되어 있음)

  • Test 관련 어노테이션을 통해 테스트 클래스를 만든다.

  • 지정된 클래스를 제외하고, 테스트를 위해 외부 클래스가 필요한 경우

    • Mock(모의) 객체 화 시키거나

    • SpringBootTest의 경우 빈 등록은 되어 있으니 @Autowired로 의존성만 부여하면 된다.

  • 테스트 코드안에 @Test 어노테이션을 붙은 테스트 메서드를 구현한다.

  • 테스트 메서드는 Given, When, Then 패턴으로 테스트 코드를 작성한다.

  • 기본적으로 AssetJ의 assertThat 함수를 이용하여 동작과 반환 값이 올바른지 확인한다.

웹 어플리케이션 통합테스트

@SpringBootTest 사용

@SpringBootTest
public class WebIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testWebApp() {
        // Given (사전 조건 설정)

        // When (테스트 실행)
        ResponseEntity<String> response = restTemplate.getForEntity("/api/resource", String.class);

        // Then (테스트 결과 검증)
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).isEqualTo("Hello, World!");
    }
}

웹 어플리케이션 전반에 대한 테스트를 위한 테스트이다.

@SpringBootTest 특성상 가상 웹 환경을 만들기에 프로젝트 클래스에 대해서 Spring이 Bean 등록을 해놨기에, 테스트에 필요한 객체는 @Autowired 통해 의존성만 부여받으면 된다.

  1. **restTemplate.getForEntity("/api/resource", String.class)**를 사용하여 /api/resource 경로로 GET 요청을 보내고, 응답을 **ResponseEntity<String>**으로 받는다.

  2. **assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK)**는 HTTP 응답의 상태 코드가 OK (200)인지를 검증한다.

  3. **assertThat(response.getBody()).isEqualTo("Hello, World!")**는 응답 본문이 "Hello, World!"인지를 검증한다.

Controller Test

@WebMvcTest 이용

@WebMvcTest(MyController.class)
public class ControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService;

    @Test
    public void testController01() throws Exception {
        // Given (사전 조건 설정)
        given(myService.getGreeting()).willReturn("Hello, World!");

        // When (테스트 실행)
        mockMvc.perform(get("/api/resource"))
            .andExpect(status().isOk())
            .andExpect(content().string("Hello, World!"));

        // Then (테스트 결과 검증)
        then(myService).should().getGreeting();
    }
}

Controller 테스트에 특화되어있는 @WebMvcTest 어노테이션으로 테스트 클래스를 만든다.

**MockMvc**는 Spring MVC 프레임워크의 가상 웹 애플리케이션 환경을 제공하며, 컨트롤러 테스트를 할 때 HTTP 요청을 보내고 응답을 검증할 수 있는 도구이다. 그래서 의존성을 주입받아 사용한다.

**MockMvc**를 주입받아 사용하면, 실제 웹 환경을 구성하지 않고도 컨트롤러의 동작을 테스트할 수 있다. **MockMvc**를 사용하여 컨트롤러의 엔드포인트(예: /api/resource)로 HTTP 요청을 보내고, 응답을 검증하여 컨트롤러의 동작을 확인한다.

Controller 에 대한 테스트이기 때문에 Service는 Mock 을 통해 가짜 객체를 만들어 사용한다.

  1. **given(myService.getGreeting()).willReturn("Hello, World!")**를 사용하여 getGreeting() 메서드가 호출될 때 "Hello, World!"를 반환하도록 설정

  2. **mockMvc.perform(get("/api/resource"))**를 사용하여 /api/resource 경로로 GET 요청을 보내고, 응답을 검증

  3. **then(myService).should().getGreeting()**를 사용하여 **myService**의 getGreeting() 메서드가 호출되었는지를 검증

Service

@ExtendWith 사용

@ExtendWith(MockitoExtension.class)
public class ServiceTest {

        @InjectMocks 
        private private MyService myService;

        @Mock 
        private MyRepository myRepository;

    @Test
    public void testService() {
        // Given (사전 조건 설정)

        // When (테스트 실행)
        String result = myService.someServiceMethod();

        // Then (테스트 결과 검증)
        assertThat(result).isEqualTo("Hello, World!");
    }
}

**@ExtendWith**는 JUnit 5의 확장 모델을 활용하여 테스트에 다양한 확장 기능을 추가하는 어노테이션이다.

@ExtendWith(MockitoExtension.class) 을 통해 Mockito 라이브러리와 함께 사용되어 Mock 객체를 생성하고 관리하는데 사용한다. 이를 통해 테스트 클래스에서 Mock 객체를 생성하고 사용하는 데 도움을 준다.

  1. @InjectMocks 어노테이션은 테스트 대상 객체를 생성하고, 대상 객체가 의존하는 다른 객체들은 @Mock 어노테이션을 이용하여 Mock으로 만들어준다. 이렇게 하면 테스트 대상 객체가 의존하는 객체를 직접 생성하지 않아도 되므로, 테스트 코드 작성이 용이해다.

    1. 만약 @InjectMocks 안쓰게 되면
    @BeforeEach
    void setUp() {
    myService= new MyService (myRepository);
    }

이와 같이 @BeforeEach 를 통해 테스트 메서드가 실행되기 전에 직접 객체를 생성할 때 의존성을 추가해줘야한다.

  1. **myService.someServiceMethod()**를 호출하여 서비스 메서드를 실행합니다. 이 메서드의 반환 값을 result 변수에 저장

  2. result 변수의 값이 "Hello, World!"와 같은지를 검증. 만약 **myService**가 **myRepository**를 호출하고 다른 동작을 수행한다면 이 부분에서 해당 동작에 대한 검증을 추가

Repository

@DataJpaTest 사

@DataJpaTest
public class RepositoryTest {

    @Autowired
    private MyRepository myRepository;

    @Test
    public void testRepository() {
        // Given (사전 조건 설정)
        MyEntity entity = new MyEntity();
        entity.setData("Hello, World!");

        // When (테스트 실행)
        myRepository.save(entity);

        // Then (테스트 결과 검증)
        MyEntity savedEntity = myRepository.findById(entity.getId()).orElse(null);
        assertThat(savedEntity).isNotNull();
        assertThat(savedEntity.getData()).isEqualTo("Hello, World!");
    }
}

**@DataJpaTest**이 어노테이션은 Spring Boot 애플리케이션에서 JPA 리포지토리를 테스트하기 위한 설정을 자동으로 제공한다. 테스트를 실행할 때 인메모리 데이터베이스를 사용하며, 엔티티 매니저, 데이터 소스, 트랜잭션 관리 등과 관련된 설정을 로드한다.

즉, JPA(Java Persistence API)와 관련된 빈들을 로드하고, 데이터베이스 관련 테스트를 수행할 수 있도록 환경을 구성하므로 Repository는 @Autowired를 통해서 의존성 주입만 하면된다.

  1. Given: 사전 조건 설정 부분으로, MyEntity 객체를 생성하고 데이터를 설정한다. 이 객체는 JPA 엔티티로, data 필드에 "Hello, World!" 문자열을 설정한다.

  2. hen: 테스트를 실행하는 부분으로, **myRepository.save(entity)**를 호출하여 엔티티를 데이터베이스에 저장한다.

  3. when: 테스트 결과를 검증하는 부분으로, **myRepository.findById(entity.getId())**를 사용하여 엔티티를 다시 조회하고, 조회한 엔티티가 null이 아닌지를 검증한다. 그리고 **assertThat(savedEntity.getData()).isEqualTo("Hello, World!")**를 사용하여 저장된 데이터가 "Hello, World!"와 일치하는지를 검증한다.

TDD 팁

단순 쿼리문에 대한 테스트는 적용하기 불가능하거나 어려웠는데, JPA나 ORM으로 어느정도 대체되어 관련 테스트가 상대적으로 매우 쉬워진 경향이 있다.

TDD & 실무

  • 처음 공부해보고 도입하려고 해보았으나, 클래스의 구성이나 프로그램 구조가 잡히지 않은 상태에서는 어려웠음

  • 여러가지로 공부해보고 실무나 주변을 본 결과 완벽한 의미의 TDD(일단 테스트 먼저 짜고 코드를 만드는 것)은 어렵다

테스트를 잘 하기 위한 기반

  • 클래스나 메서드가 SRP를 잘 지키고, 크기가 적절히 작아야 함

    • 그래야 테스트를 집중력 있게 만들 수 있고 한 메서드에 너무 많은 테스트를 수행하지 않아도 됨

    • 이게 테스트를 하는 것의 장점이 되기도 함(테스트를 하면 자연스럽게 역할이 확인되면서 쪼개짐)

  • 적절한 Mocking을 통한 격리성 확보

    • 단위테스트가 만능은 아니지만, 위의 SRP처럼 해당 메서드의 역할을 정확히 테스트하려면 주변 조건을 적절히 통제해야 한다.
  • 당연히 잘 돌겠지라는 생각말고 꼼꼼히 테스트 && 너무 과도하게 많은 테스트와 코드량이 생기지 않도록 적절히 끊기

    • 테스트코드도 코드 리뷰 시에 적절한 테스트를 하는지 확인 필요
  • 테스트 코드 개선을 위한 노력

    • 테스트코드도 리팩토링 필요

    • 테스트코드의 기법들도 지속적인 고민 필요(통합테스트 등)


일단 다시 파악한 내용 정리

  1. 단위 테스트(Unit Test):

    • 대상: 개별 컴포넌트, 클래스, 또는 메서드.

    • 목적: 개별 컴포넌트의 로직을 분리하여 테스트하여 버그를 신속하게 찾아내고 수정합니다.

    • 테스트 대상: 주로 서비스 클래스, 유틸리티 클래스, 비즈니스 로직.

    • 환경: 주로 Mock 객체를 사용하여 외부 의존성을 대체하고, 테스트 환경을 간소화합니다.

  2. 통합 테스트(Integration Test):

    • 대상: 다수의 컴포넌트 또는 모듈 간의 상호작용.

    • 목적: 컴포넌트 간의 통합 동작을 검증하여 시스템 전체의 정상 작동을 확인합니다.

    • 테스트 대상: 주로 컨트롤러, 리포지토리, 데이터베이스 연동, 외부 서비스 연동 등.

    • 환경: 실제 의존성 및 데이터베이스를 사용하여 실제 환경과 유사하게 테스트합니다.

  3. 컨트롤러 테스트(Controller Test):

    • 대상: 스프링 MVC 컨트롤러.

    • 목적: HTTP 요청과 응답을 테스트하여 웹 요청 처리 및 뷰 렌더링 동작을 확인합니다.

    • 테스트 대상: 컨트롤러 메서드, URL 매핑, 뷰 템플릿.

    • 환경: **MockMvc**를 사용하여 HTTP 요청 및 응답을 시뮬레이트하고, 실제 의존성을 Mock으로 대체합니다.

  4. 스프링 부트 테스트(Spring Boot Test):

    • 대상: 스프링 부트 애플리케이션.

    • 목적: 스프링 부트 애플리케이션의 전체적인 동작을 테스트하여 애플리케이션의 통합성을 검증합니다.

    • 테스트 대상: 애플리케이션 구성, 빈 등록, 설정 파일.

    • 환경: 스프링 부트 테스트 어노테이션(@SpringBootTest, @AutoConfigureMockMvc 등)을 사용하여 애플리케이션 컨텍스트를 설정하고, 실제 서버 또는 내장 서버를 실행할 수 있습니다.

  5. 기타 테스트: 스프링 부트는 다양한 테스트 라이브러리와 기능을 지원하며, 예를 들어 스프링 시큐리티 테스트, 데이터 JPA 테스트, REST API 테스트, WebSocket 테스트 등 다양한 테스트 시나리오를 다룰 수 있습니다.

서비스 단 테스트는 단위테스트로

@ExtendWith(MockitoExtension.class)

@InjectMocks

@Mock

이것들을 통해 의존성 처리하고 진행

컨트롤러 단 테스트는 보통은 통합테스트(Controller Test)

컨트롤러가 웹 애플리케이션의 클라이언트와 서버 사이의 인터페이스 역할을 하고, HTTP 요청 및 응답 처리, URL 매핑, 뷰 렌더링 등과 같은 웹 관련 동작을 수행하기 때문에 특수한 경우(특정한 메서드의 동작을 집중적으로 검증이 필요한 경우 같은) 단위테스트 보다는 통합테스트로 운영하는 경우가 많다.

@WebMvcTest(ArticleController.class) 어노테이션은 특정 컨트롤러를 대상으로 하는 웹 계층 테스트를 실행하도록 Spring에 알려줍니다. 이 클래스와 관련된 핵심 요소와 역할은 다음과 같습니다:

  1. @WebMvcTest(ArticleController.class): 이 어노테이션은 ArticleController 클래스를 대상으로 하는 웹 계층 테스트를 위해 Spring ApplicationContext를 설정합니다. 이 테스트는 웹 요청과 응답을 시뮬레이션하고 해당 컨트롤러의 동작을 테스트하는 데 사용됩니다.

  2. MockMvc mvc: **MockMvc**는 Spring MVC 컨트롤러의 테스트를 위한 가상의 웹 환경을 제공하는 객체입니다. **MockMvc**를 사용하여 HTTP 요청을 생성하고 컨트롤러의 응답을 검증할 수 있습니다. 이것은 테스트할 컨트롤러와 상호 작용하기 위한 핵심 도구입니다.

  3. ArticleControllerTest 생성자: 이 생성자는 테스트 클래스가 시작될 때 Spring이 자동으로 호출합니다. @Autowired 어노테이션이 붙어 있으므로 Spring 컨테이너에서 **MockMvc**와 **FormDataEncoder**를 주입합니다. 이들은 테스트에서 사용할 수 있는 필드입니다.

    1. 더 자세

ArticleControllerTest 클래스의 생성자에서 @Autowired 어노테이션을 사용하여 **MockMvc**와 **FormDataEncoder**를 다시 주입하는 이유는 테스트 클래스에서 이들 객체를 직접적으로 만들거나 초기화하지 않고 Spring에 의해 관리되도록 하기 위함입니다. 또한 @Autowired 로 바로 객체를 만들지 않은 이유 테스트 코드가 동작할때만 의존성을 주입하기 위한 것 안그러면 원본 프로젝트 실행시에도 의존성이 주입되어 자원낭비, 예외가 발생할 수 있다.

  1. **private final MockMvc mvc;**와 **private final FormDataEncoder formDataEncoder;**은 클래스 내에서 사용되는 멤버 변수입니다. 이들은 테스트 메서드에서 사용되어야 하며, 테스트 클래스가 Spring으로부터 관리되고 주입되는 빈으로 설정되어야 합니다.

  2. @Autowired 어노테이션을 사용하면 Spring은 해당 클래스를 자동으로 검색하고 필요한 빈을 주입합니다. 이는 Spring 컨테이너에서 관리되는 빈을 테스트 클래스에 주입하려는 목적을 가집니다. 따라서 **MockMvc**와 **FormDataEncoder**가 다른 곳에서 미리 생성되고 Spring에 의해 관리되고 있을 때, 이 생성자를 사용하여 해당 객체를 주입받을 수 있습니다.

  3. 이렇게 하면 ArticleControllerTest 클래스가 **MockMvc**와 **FormDataEncoder**를 직접 생성하거나 초기화할 필요가 없으며, Spring에 의해 관리되는 빈으로 자동으로 주입됩니다. 이렇게 하면 코드 중복을 줄이고 관리를 편리하게 할 수 있으며, 또한 Spring의 의존성 주입(Dependency Injection) 메커니즘을 활용하여 테스트 환경을 설정할 수 있습니다.

요약하면, **@Autowired**를 사용하여 **MockMvc**와 **FormDataEncoder**를 주입받는 것은 Spring의 의존성 주입을 활용하여 객체를 관리하고 테스트 환경을 설정하기 위한 방법입니다. 이렇게 하면 테스트 클래스가 Spring의 관리 아래에서 빈으로 관리되며, 필요한 의존성을 자동으로 주입받을 수 있습니다.

  1. @MockBean private ArticleService articleService;@MockBean private PaginationService paginationService;: 이 두 개의 어노테이션은 Spring의 MockBean 기능을 사용하여 **ArticleService**와 **PaginationService**의 가짜(mock) 빈을 생성합니다. 테스트에서는 이러한 가짜 빈을 사용하여 컨트롤러가 서비스와 상호 작용하는 방식을 확인할 수 있습니다.

테스트 클래스의 주요 목적은 **ArticleController**가 요청을 올바르게 처리하고 서비스와 상호 작용하는지 확인하는 것입니다. 이를 위해 **MockMvc**를 사용하여 HTTP 요청을 모방하고 **@MockBean**으로 생성한 가짜 서비스 빈을 통해 컨트롤러와의 상호 작용을 관찰하고 검증합니다. 이를 통해 컨트롤러의 동작이 예상대로 이루어지는지 확인할 수 있습니다.

Service

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
@DisplayName("MemberService 테스트")
class MemberServiceTest {

    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;

    @Test
    @DisplayName("중복 회원 생성시 예외 발생 - 이메일 조회")
    void createMemberException() {
        // Given
        Long memberId = 1L;
        Member testMember = createTestMember(memberId);
        when(memberRepository.findByEmail(anyString())).thenReturn(Optional.of(testMember));

        // When
        Throwable throwable = catchThrowable(() -> memberService.createMember(
                Member.builder()
                        .email("PreventNull")
                        .build()));

        // Then
        assertThat(throwable)
                .isInstanceOf(ServiceLogicException.class)
                .hasMessageContaining(ErrorCode.MEMBER_EXISTS.getMessage());
    }

    // 테스트에서 사용할 가짜 Member 객체 생성 메서드
    private Member createTestMember(Long memberId) {
        return Member.builder()
                .id(memberId)
                .email("test@example.com")
                .build();
    }
}
  • 어노테이션: **@SpringBootTest**는 Spring Boot 애플리케이션을 통합 테스트하기 위한 어노테이션입니다. 실제 애플리케이션 컨텍스트를 로드합니다.

  • 객체:

    • UserService: 비즈니스 로직을 제공하는 서비스 클래스.

    • UserRepository: 실제 레포지토리 객체를 사용하기 위한 의존성.

  • 메서드:

    • findUserByUsername(): 사용자 이름을 기반으로 사용자를 조회하는 서비스 메서드.

    • when(): Mockito에서 사용되는 메서드로, 메서드 호출 시 특정 동작을 정의합니다.

    • thenReturn(): Mockito에서 사용되는 메서드로, **when()**과 함께 사용되어 가짜 데이터를 반환합니다.

Controller

비즈니스 로직 부분으로, 기능, 데이터 조작하는 부분에 집중한다.

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Mock
    private UserService userService;

    @InjectMocks
    private UserController userController;

    @Test
    public void testGetUserByUsername() throws Exception {
        // 가짜 데이터 생성
        User johnDoe = new User("JohnDoe", "john@example.com");

        // userService.findUserByUsername("JohnDoe")가 호출될 때 가짜 데이터 반환
        Mockito.when(userService.findUserByUsername("JohnDoe")).thenReturn(johnDoe);

        // GET /user/JohnDoe 요청 수행 및 결과 검증
        mockMvc.perform(MockMvcRequestBuilders.get("/user/JohnDoe"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("JohnDoe"))
                .andExpect(MockMvcResultMatchers.jsonPath("$.email").value("john@example.com"));
    }
}
  • 어노테이션: **@WebMvcTest(UserController.class)**는 Spring 웹 애플리케이션 컨트롤러를 테스트하기 위한 어노테이션입니다. 해당 컨트롤러와 관련된 빈만 로드합니다.

  • 객체:

    • UserController: 웹 요청과 응답을 처리하는 컨트롤러 클래스.

    • UserService: 서비스 객체를 사용하기 위한 의존성.

    • MockMvc: Spring MVC 테스트를 위한 가짜 웹 환경을 제공하는 객체.

  • 메서드:

    • perform(): MockMvc를 사용하여 HTTP 요청을 수행하고 결과를 검사합니다.

    • andExpect(): 예상 결과를 검사하는 메서드.

    • status(): HTTP 응답 상태 코드를 검사합니다.

    • jsonPath(): JSON 응답 내의 특정 값을 검사합니다.

Repository

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void testSaveUser() {
        User user = new User("JohnDoe", "john@example.com");
        userRepository.save(user);

        User savedUser = userRepository.findByUsername("JohnDoe");
        assertNotNull(savedUser);
        assertEquals("JohnDoe", savedUser.getUsername());
        assertEquals("john@example.com", savedUser.getEmail());
    }
}
  • 어노테이션: **@DataJpaTest**는 Spring Data JPA 레포지토리를 테스트하기 위한 어노테이션입니다. 데이터베이스 관련 테스트 환경을 설정합니다.

  • 객체: **UserRepository**는 실제 JPA 엔터티를 조작하는 메서드를 가진 레포지토리 인터페이스입니다.

  • 메서드:

    • save(): JPA 엔터티를 저장합니다.

    • findByUsername(): 사용자 이름을 기반으로 사용자를 조회합니다.

    • assertNotNull(): 객체가 null이 아닌지 검사합니다.

    • assertEquals(): 두 값이 같은지 비교합니다.

O
Ok JaeOok2y ago

많이 배우고 갑니다. 감사합니다!