본문 바로가기

개발기술/빌드, 배포, 인프라

Test 코드 작성

테스트 시작 전 점검

Fuctionaly Prototype comes First. 테스트 코드를 아무리 잘짜더라도 특정한 기능이 Missing되는 부분은 테스트 코드로 잡아줄 수가 없다. 우선은 요구하는 기능이 제대로 동작하는지 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 클래스를 테스트하는 테스트 클래스 생성
  • @Springboot Test : 테스트 기능 동작을 위해서, 클래스가 의존하고 있는 의존성을 주입하기위해서 bean을 주입받는다. 이를 위해  context loader(Spring환경)을 만들어서 @AutoWired로 bean을 주입함. 
  • ` : 기존 생성된 bean 의존성을 주입하는 것이 아니라 격리를 통해서 mock을 만들고 싶을때 사용.

2. 테스트 메소드 생성

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

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

    @Autowired
    private AccountService accountService;
    @Autowired
    private AccountRepository accountRepository;
    @Autowired
    private AccountUserRepository accountUserRepository;

    @BeforeEach
    void init() {
        accountUserRepository.save(AccountUser.builder().id(1L).name(
                "proro").build());
    }

    @Test
    void testCreateAccount1() {
        AccountDto accountdto = accountService.createAccount(1L, 1000L);

        assertEquals("1000000000", accountdto.getAccountNumber());
        assertEquals(1000L, accountdto.getBalance());
    }

 

 

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(param)) : 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

 

  • gievn() : Used to specify the behavior of a mock when a method is called.
    • willreturn() : Specifies the return value when the mocked method is called.
    • willThrow() : Specifies an exception to be thrown when the mocked method is called.
  • verify() : Verifies if a method on a mock object was called and optionally checks the number of times it was called.
  • times() : Used in conjunction with verify() to check how many times a method was called.
  • never() : Verifies that a method was never called.
  • any() / anyInt(), anyString(),  etc. : Matchers used to specify that the method can be called with any value of a certain type (e.g., any integer, any string).
  • any(MyClass.class) : to match any instance of a specific class
  • ArgumentCaptor : Captures the arguments that were passed to a mock's method for further assertions.
 

 

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를 리턴 하는 클래스인데 해당 클래스의 역할이 제대로 되고 있는지 확인하는 테스트.

 

방법1 : @SpringBootTest + @AutocomfigureMockMvc

  • @SpringBoottest를 통해서 전체 Bean을 생성한 후, MockMvc에 필요한 Bean들을 주입함.
    • 모든 Bean이 활용되는 만큼, 격리성 확보가 어려운 단점이 있어 WebMvcTest가 선호되는 추세이다.
  • mockMvc를 통해  http 요청(perform(getmethod))과 응답검증(andExpect(jsonpath,status())을 진행

 

방법2 : @WebMvcTest

  • 격리성 확보를 위해서 내가 필요로 하는 MVC관련 Bean들만 생성 (Controller, ControllerAdvice, Converter, Filter, Handlerinterceptor 등)
  • @WebMvcTest(test할 컨트롤러.class)를 명시한 후,  Controller의 의존성은 @MockBean을 통해 주입하고 @autowired를 통해서 Mockmvc를 호출주입한다.
  • 그 후, mock object를 생성하기 위해서 given(inputvalue).willReturn(outputvalue)로 원하는 동작을 하도록 셋팅 한다.  (Mockito와 유사한 방식)
  •  mockMvc를 통해  http 요청(perform(getmethod))과 응답검증(andExpect(jsonpath,status())을 진행
mockMvc.perform(post("/account")
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(
                new CreateAccount.Request(1L,100L)
        )))

MockMvcRequest생성자/objectMapper상ㅅㅇ

MockMvc세부 클래스 및 메소드

 

  • MockMvc.perform(RequestBuilder requestBuilder): This method is used to perform an HTTP request using the given RequestBuilder.
    • MockMvcRequest : builder 패턴(MockHttpServletRequestBuilder)을 사용해서 post url, contents type, content를 입력하여 MockMvcRequest를 생성함.  
  • MockMvc.perform() -> ResultAction : ResultActions provides a chainable API for adding expectations, printing results, and handling the response of the mock request.
    • The .andExpect(ResultMatcher) method allows you to add assertions about the response
    • The .andDo(ResultHandler) method lets you perform additional actions with the response, such as printing the response to the console with print() or other
  • MockMvcResultMatchers
    • MockMvcResultMatchers.status(): to create a ResultMatcher that matches the HTTP status of the response. 
    • MockMvcResultMatchers.jsonPath(String expression, Object... args): to creates a ResultMatcher that applies a JSONPath expression to the response content
      • JsonPath Expression : JSONPath is a query language for JSON, similar to XPath for XML. It allows you to navigate through elements and attributes in a JSON document
        • Root Node ($):
        • Child Nodes (. or []):
  • MockMvcResultHandlers
    • MockMvcResultHandlers.print() : returns a ResultHandler that writes the request and response details to the standard output
  • ObjectMapper is part of the Jackson library, which is widely used for processing JSON data in Java. In the context of testing Spring MVC controllers, it serves to serialize and deserialize JSON data.
    • objectMapper.writeValueAsString(...) is used to convert a Java object into a JSON string

 

@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());

 

서비스 테스트

  • 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)

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

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

 

 

'개발기술 > 빌드, 배포, 인프라' 카테고리의 다른 글

Git 그리고 GitHub  (0) 2024.09.16
SQL과 NoSQL  (0) 2024.09.02
인프라 확장  (0) 2024.09.02
스프링 부트 환경설정 (스프링 Init, Package, Configuration)  (0) 2024.07.23
Command-Line Instructions(CLI)  (0) 2024.01.24