본문 바로가기

개발기술/테스트, 인프라

Unit Test 코드 작성

테스트 시작 전 점검

Function Test comes First. 테스트 코드를 아무리 잘짜더라도 특정한 기능이 Missing되는 부분은 테스트 코드로 잡아줄 수가 없다. 우선은 요구하는 기능이 제대로 동작하는지 API Manual Testing이 선행되고 CoreFunction이 제대로 동작하는지 확인이 완료된 후에 테스트코드를 진행한다.

테스트의 중요성

과거

  • 1. 방식 : 자동화하기 어려운 sql중심의 코드들이 많아, 테스트케이스를 개발자가 직접 만들고, 기능을 동작시켜보는 식으로 수동적으로 진행. 
  • 2.관점 : 해당 방식은 1회성 외주 개발 후 철수하는 방식으로 진행되어, 코드의 품질보다는 기능적 완성도만 점검하는 방식이며 코드의 유지보수성에 대해서는 중요성이 낮았음.
  • 3.단점 : 기능이 바뀌면 새로 테스트를 진행해야하여 테스트 진행이 어려움

현재 

  • 방식 : 자동화된 테스트 방식이 선호도가 높아짐
  • 관점 : 코드의 품질, 유지보수성, 기능적 완성도 종합적으로 확인

 

유닛 테스트 진행의 장점

  • 테스트를 짜면서 자신의 코드를 자연스럽게 셀프코드 리뷰를 하게됨
    • Exception case확인 : 코드의 아주 세부적인 정책들을 문서화처럼 코드로 구체화할 수 있음. 기획단계에서 다 커버되지 않은 정책들을 추가적으로 커버해줄 수 있음. 
    • 테스트 짜기가 어려울 때 : 너무 역할이 과도하게 부여된 것이 아닌지?
    • 테스트 잘짜고난 후 : 리팩터링 했을 때도 문제상황을 바로 확인가능해서 대처가능

 

유닛 테스트를 잘하려면

  • SRP가 잘짜여진 코드를 짜야한다
  • Mock을 활용하여 격리성을 확보한다.
  • 테스트 커버리지를 최대한 높인다.
  • 테스트코드도 프로그래밍 코드와 동일하게 중요한 부분임을 인지하고, 가시성 등을 고려할것.

 

 

 

 

 

 

 

JUNIT(Java UnitTest)

  유닛테스트 프레임워크 일환으로 Java용으로 개발된 프레임워크. 내가 형식에 맞추어 만들어놓은 코드를 단위테스트로 실행하고 결과를 검증해서 전체 결과를 리포트 해줌. 단위"는 함수, 메서드, 클래스와 같은 작은 부분을 의미하며, 각각의 단위가 올바르게 동작하는지를 독립적으로 확인하는 테스트를 말함. spring-boot-starter-test에 기본적으로 Junit이 포함됨. 

  단위테스트는 독립적인 테스트로, 다른 클래스의 기능이나 DB의 동작에 영향을 받아서는 안된다. 그렇기때문에 DB를 사용하더라도 실제 DB 데이터와는 격리되어있고 beforeeach에서 DB에 testcase를 넣어두고 이를 사용하여 테스트한다. 독립적인 테스트를 전제로 하기때문에 Class가 독립적인 것이 아니라면 Junit은 Mock이 필수적이다.

 

Junit 테스트 과정

 

  • Write tests in the form of methods, Use annotations to define test methods, setup, and teardown.
  • Assert expected results (output) from your code based on given inputs.
  • Run and report the results of those tests (pass/fail).

 

1. 테스트 클래스 생성

테스트 메소드를 담고 있는 클래스로서, 테스트 메소드는 하나의 테스트 메소드당 결과 시나리오가 여러개로 여러개의 메소드가 있을 수 있다.  

  • ctrl + shift + T : Junit 클래스를 테스트하는 테스트 클래스 생성
  • mock : 기존 생성된 bean 의존성을 주입하는 것이 아니라 격리를 통해서 mock을 만들고 싶을때 사용.

2. 테스트 메소드 생성

테스트 메소드는 테스트하고자하는 로직이 들어가있는 메소드로, 주로 테스트하고자하는 클래스를 호출하여 testcase 입력값을 넣고, testcase 결과값이 나오는지 assert해보는 것으로 구성된다.

  • @Test : 메소드를 test로 등록해서 Junit framework에 주입함. (현재 Intelij에서 test라는 축약어로 보일러코드 축약해놓음)
  • @beforeEach : 매 @Test를 실행시키기 전에 기초작업을 해야한다면, Beforeeach에 넣어서 이를 실행시키고 테스트를 진행한다.

 

3.  테스트 결과 확인

  • assertSame(a,b) : a,b : 두 값이 같다는 것을 테스트. 객체자체가 같은지 확인
  • assertEquals(a,b) : a,b equals : 객체의 값이 같은지 확인
  • - assertArrayEquals(a,b) : a, b 배열이 같은 값인지 확인
  • - assertTrue(a) : a가 참인지 확인
  • - assertNotNull(a) :  a가 null인지 확인
  • AssertThrows(exception.class, () -> 실행시킬 method(파라미터)) : 1. 특정 method를 실행시킨다. 2. 그리고 특정 method가 실행했을때 exception.class로 지정된 예외를 던지는지 테스트하며 그 return 값을 반환받는다

 

Mockito

 

Manual Test의 문제점

1. 위의 예시와 같이 Input과 예상결과값을 하나하나 입력하기에 번거로움

2. 여러가지 의존성이 엮여있다보니 fail이 나도 의존성이 문제인지 코드의 문제인지 식별불가함(단위테스트 불가)

 

  위 문제를 해소하기 위해서 Mock이라는 가상의 비어있는 객체를 만들고, 그 객체가 내가 원하는 방식으로 동작(내가 지정한 특정한 값이 input되면 특정값을 반환하도록) 동작하게하여 의존성으로부터 단위 격리성을 확보하자.

 

  Mock사용의 장점

1. Conditional Responses: input에 따라서 다양한 값을 반환하도록 조정가능

2. Verification of Interactions: input과 return동작을 함으로써 dependency와 올바른 교류(동작)를 하는지 확인가능하다.

 

Mock을 생성하기 위한 환경설정

@Mock : 클래스를 mock개체로 만들어 주어 input과 output을 직접 설정할 수 있도록 한다.

@InjectMocks : 해당 클래스의 instance를 만들고 mock으로 만든 개체를 의존성으로  주입시켜준다. 

@ExtendWith(MockitoExtension.class) : mockito를 해당클래스에 적용시켜준다. @Mock, @InjectMocks 표시된 객체들을 생성해준다. 해당 annotaion이 없으면 Mockannotaion.OpenMocks()을 사용해서 생성해줘야함.

 

Mock Method

 

  • when().thenReturn()
  • given().willReturn() : Mock 메서드가 호출되었을 때 반환할 값을 지정합니다.
  • given().willThrow() : Mock 메서드가 호출되었을 때 예외를 던지도록 지정합니다.
  • verify() : Mock 객체의 메서드가 호출되었는지 확인하고, 필요한 경우 호출 횟수도 검증할 수 있습니다.
    • times() : verify()와 함께 사용되어 메서드가 호출된 횟수를 확인합니다.
    • never() : 메서드가 전혀 호출되지 않았음을 검증합니다.
  • any() / anyInt(), anyString() 등 : 특정 타입의 어떤 값이든 메서드에 전달될 수 있도록 지정하는 매처입니다. 예를 들어, anyInt()는 어떤 정수든 허용합니다. 가능하면 쓰지 않는것이 좋다.
  • any(MyClass.class) : 특정 클래스의 어떤 인스턴스든 허용하기 위해 사용됩니다. 가능하면 쓰지 않는것이 좋다.
  • ArgumentCaptor : Mock 메서드에 전달된 인수를 캡처하여 이후에 추가적인 검증을 할 수 있도록 합니다.
 

 

test방식

given - assert

@ExtendWith(MockitoExtension.class)
class AccountServiceTest {

    @Mock
    private AccountRepository accountRepository;

    @InjectMocks
    private AccountService accountService;

    @Test
    @DisplayName("계좌조회성공")
    void testCreateAccount() {
        //given
        given(accountRepository.findById(anyLong())).willReturn(Optional.of(
                Account.builder()
                        .id(1L)
                        .accountNumber("1000000000")
                        .balance(1000L)
                        .accountStatus(IN_USE)
                        .build()));

        //when
        Account account = accountService.getAccount(9898L);

        //then
        assertEquals("1000000000", account.getAccountNumber());
        assertEquals(1000L, account.getBalance());
    }

 

컨트롤러 테스트 방법

컨트롤러는 url주소와 body, parameter을 통해 argument를 받고 response를 리턴 하는 클래스라고할 수 있으며 해당 클래스가 역할을 제대로 하고 있는지 확인하는 테스트.

 

@WebMvcTest 사용법 요약

@WebMvcTest : WebMvcTest는 Spring MVC의 특정 레이어만 테스트하는 데 유용한 애너테이션입니다. 필요한 MVC관련 Bean들만 생성(예: Controller, ControllerAdvice, Converter, Filter, HandlerInterceptor 등)하여 격리된 테스트 환경을 제공합니다. 

@WebMvcTest(AccountController.class)
class AccountControllerTest {

    @MockBean
    private AccountService accountService;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void successCreateAccount() throws Exception {
        //given
        given(accountService.createAccount(anyLong(),anyLong()))
                .willReturn(AccountDto.builder()
                        .userId(1L)
                        .accountNumber("1234567890")
                        .registeredAt(LocalDateTime.now())
                        .unregisteredAt(LocalDateTime.now())
                        .build());
        //when//then
        mockMvc.perform(post("/account")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(
                        new CreateAccount.Request(1L,100L)
                )))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.userId").value(1))
                .andExpect(jsonPath("$.accountNumber").value(1234567890))
                .andDo(print());

 

1. 애너테이션 지정

  • @WebMvcTest(테스트할 컨트롤러.class)을 명시해 특정 컨트롤러만 로딩합니다.
  • @mockuser을 통해서 userdetail mock을 생성함

2.  Mocking을 통한 의존성 주입

 

  • 컨트롤러에서 의존하는 빈들은 실제 빈 대신 @MockBean을 사용해 가짜(Mock) 객체로 주입합니다.
  • @Autowired를 통해 MockMvc를 주입 받아 HTTP 요청을 테스트합니다.

3. Mock 동작 설정

  • given(inputValue).willReturn(outputValue) 형식을 사용하여 특정 상황에서의 Mock 동작을 지정합니다. (Mockito와 유사한 방식입니다.)

4. HTTP 요청 및 응답 검증

  • mockMvc.perform()을 통해 HTTP 요청을 보내고, andExpect()를 통해 응답 상태나 JSON 응답 본문 등의 검증을 진행합니다.
    예를 들어, mockMvc.perform(get("/api/path")).andExpect(status().isOk())와 같이 검증할 수 있습

 

MockMvc세부 클래스 및 메소드

  • MockMvc.perform(RequestBuilder requestBuilder): 이 메소드는 주어진 RequestBuilder를 사용하여 HTTP 요청을 수행합니다.
  • MockMvcRequest : 빌더 패턴(MockHttpServletRequestBuilder)을 사용하여 post URL, content 타입, content 내용을 설정하여 MockMvcRequest 객체를 생성합니다.
  • MockMvc.perform() -> ResultAction : esultActions는 체인 형식의 API를 제공하여 기대값 설정, 결과 출력 및 모의 요청 응답 처리를 추가할 수 있게 해줍니다.
    • .andExpect(ResultMatcher): 응답에 대한 Assertion을 추가합니다.
    • .andDo(ResultHandler): 응답을 사용하여 추가 작업을 수행할 수 있게 해줍니다. 예를 들어 print() 메소드를 사용해 콘솔에 응답 내용을 출력할 수 있습니다.
  • MockMvcResultMatchers
    • MockMvcResultMatchers.status(): 응답의 HTTP 상태 코드에 대한 ResultMatcher를 생성합니다. 
    • MockMvcResultMatchers.jsonPath(String expression, Object... args): 응답 내용에 JSONPath 표현식을 적용하는 ResultMatcher를 생성합니다.
      • JSONPath 표현식 : JSONPath는 JSON을 위한 쿼리 언어로, XML의 XPath와 유사합니다. JSON 문서 내의 요소와 속성을 탐색할 수 있습니다.
        • 루트 노드 ($): JSON의 최상위 노드를 나타냅니다.
        • 자식 노드 (. 또는 []): 특정 자식 요소를 탐색하는 데 사용됩니다.
  • MockMvcResultHandlers
    • MockMvcResultHandlers.print(): 요청과 응답의 세부 정보를 표준 출력에 출력하는 ResultHandler를 반환합니다.

 ObjectMapper

  • Jackson 라이브러리의 일부로, Java에서 JSON 데이터를 처리할 때 널리 사용됩니다. Spring MVC 컨트롤러 테스트에서 주로 JSON 데이터를 직렬화 및 역직렬화하는 데 사용됩니다.
    • objectMapper.writeValueAsString(...): Java 객체를 JSON 문자열로 변환하는 데 사용됩니다.

 

서비스 테스트

  • verify : 의존하고 있는 mock이 해당되는 동작을 얼마나 수행했는지 검증하는 방법. 검증하고자하는 클래스가 depedency인 mock과 얼마나 interaction을 수행했는지 검증하는 기법
 //then

    verify(memberRepository, times(1))
            .save(any(MemberDetails.class));
}

 

  • ArgumentCaptor : 의존하고있는 Mock이 전달받은 데이터를 검증하기 위해서, captor이라는 데이터를 담는 빈박스를 만들고 데이터를 담은 후 검증함. 이는 의존성의 로직을 실제로 동작시킬 수 없기에, 의존성과 상호작용하는 service logic이 옳은 값을 input 하는지 확인하기 위함이다. 

1. Declare and Create an ArgumentCaptor:

2. Use the Captor in a Verification

3.Retrieve and Assert Captured Values:

ArgumentCaptor<Account> captor = ArgumentCaptor.forClass(Account.class);
verify(accountRepository, times(1)).save(captor.capture());
assertEquals("1000000013", captor.getValue().getAccountNumber());

 

UserEntity userEntity = UserEntity.builder()
        .id(1L)
        .userId(userId)
        .userName("Test User")
        .phoneNumber("1234567890")
        .build();

UserEntity userEntity1 = mock(UserEntity.class);
  • Class<T> expectedType: This parameter specifies the type of the exception you expect to be thrown.
  • Executable executable: This is a functional interface that JUnit uses to pass the code that is expected to throw the exception. An Executable is a simple interface with a single method void execute() throws Throwable. you pass a lambda expression or a method reference that fits this functional interface.

 

 

테스트 주도 개발(TDD, Test Driven Dev)

테스트 코드를 먼저 만든 후에 그 테스트 코드를 통과하기위해 작성하는 코드 작성법

개발의 목적성이 매우 목적해지기때문이, 이 테스트코드로 인해서 개발목적을 명확하게 한다는 것. 

 

 

'개발기술 > 테스트, 인프라' 카테고리의 다른 글

Prometheus 모니터링 + Grafana  (0) 2025.01.26
대규모 서비스는 어떻게 서비스되는가  (0) 2025.01.09
모니터링 툴  (0) 2025.01.05
AWS 서비스  (0) 2025.01.02
데이터베이스 스케일 업 & 아웃  (0) 2024.09.02