테스트

InSeok·2022년 9월 22일
0

TIL

목록 보기
36/51

목차


  1. 단위 테스트
  2. 슬라이스 테스트
  3. JUnit
  4. Hamcrest
  5. Mockito
  6. TDD

배운 내용


**단위 테스트(Unit Test)**

기능 테스트

  • 애플리케이션을 사용하는 사용자 입장에서 애플리케이션이 제공하는 기능이 올바르게 동작하는지를 테스트
    • 일반적으로는 테스트 전문 부서(QA 부서) 또는 외부 QA 업체등 개발자가 아닌 제 3자가 진행

통합 테스트

  • 클라이언트 측 툴 없이 개발자가 짜 놓은 테스트 코드를 실행

슬라이스 테스트

  • 애플리케이션을 특정 계층으로 쪼개어서 하는 테스트
  • API 계층, 서비스 계층, 데이터 액세스 계층이 각각 슬라이스 테스트의 대상이 된다.
  • Mock(가짜) 객체를 사용해서 계층별로 끊어서 테스트 할 수 있다.

단위 테스트

  • 단위 테스트 코드는 메서드 단위로 대부분 작성된다.
  • 데이터베이스의 상태가 테스트 이 전과 이 후가 동일하게 유지될 수 있다면 데이터베이스가 연동된다고 해도 단위 테스트에 포함될 수 는 있다.
  • 단위 테스트는 최대한 독립적인 것이 좋고, 최대한 작은 단위인 것이 더 좋다.

테스트 케이스(Test Case)

  • 테스트를 위한 입력 데이터, 실행 조건, 기대 결과를 표현하기 위한 명세
  • 단위를 테스트하기 위해 작성하는 테스트 코드

**F.I.R.S.T 원칙**

Fast(빠르게)

  • 작성한 테스트 케이스는 빨라야 한다

Independent(독립적으로)

  • 각각의 테스트 케이스는 독립적이어야 한다
  • 일반적으로 클래스 단위로 해당 클래스 내의 메서드 동작을 테스트 하는데, 메서드는 여러개 존재할 가능성이 높을테니 테스트 클래스 안에 테스트 케이스도 하나 이상이다. 이때, 어떤 테스트 케이스를 먼저 실행시켜도 실행되는 순서와 상관없이 정상적인 실행이 보장 되어야 한다.

Repeatable(반복 가능하도록)

  • 테스트 케이스는 어떤 환경에서도 반복해서 실행이 가능해야 된다

Self-validating(셀프 검증이 되도록)

  • 단위 테스트는 성공 또는 실패라는 자체 검증 결과를 보여주어야 한다

Timely(시기 적절하게)

  • 단위 테스트는 테스트 하려는 기능 구현을 하기 직전에 작성해야 한다
  • 구현하고자 하는 기능을 단계적으로 조금씩 업그레이드하면서 그때 그때 테스트 케이스 역시 단계적으로 업그레이드 하는 방식이 더 낫다

given - when - then

Given

  • 테스트를 위한 준비 과정을 명시
  • 테스트 대상에 전달되는 입력 값(테스트 데이터) 역시 Given에 포함

When

  • 테스트 할 동작(대상)을 지정

Then

  • 테스트의 결과를 검증하는 영역
  • 일반적으로 예상하는 값(expected)과 테스트 대상 메서드의 동작 수행 결과(actual) 값을 비교해서 기대한대로 동작을 수행하는지 검증(Assertion)하는 코드들이 포함

Assertion(어써션)은 ‘예상하는 결과 값이 참(true)이길 바라는 것

**JUnit**

  • Java 언어로 만들어진 애플리케이션을 테스트 하기 위한 오픈 소스 테스트 프레임워크
  • JUnit으로 작성한 테스트 케이스는 항상 일정한 순서로 테스트 케이스가 실행된다는 보장이 없다.

JUnit 기본구조

  • void 타입의 메서드 하나 만들고, @Test 애너테이션을 추가
    • 내부에 테스트 하고자하는 대상 메서드에 대한 테스트 로직작성
  • @DisplayName("Hello JUnit Test") : 실행결과 창에 표시되는 이름 설정

**Assertion 메서드**

  • assertEquals(expected, actuals) : 기대하는 결과값(expected)와 실제 결과값(actual)이 일치하는지 검증
  • assertNotNull(테스트대상객체, 실패시 표시할메시지) : Null 여부 테스트
  • assertThrows(발생이 기대되는 예외클래스 () → 테스트 대상 메서드) : 예외(Exception) 테스트
  • 예외 클래스의 상속 관계를 이해한 상태에서 테스트 실행 결과를 예상해야 된다

**테스트 케이스 전후 처리**

  • @BeforeEach@BeforeAll()
  • @AfterEach, @AfterAll

**Assumption을 이용한 조건부 테스트**

  • assumeTrue()메서드는 파라미터로 입력된 값이 true이면 나머지 아래 로직들을 실행

Hamcrest

  • JUnit 기반의 단위 테스트에서 사용할 수 있는 Assertion Framework

사용하는이유

  1. 매쳐(Matcher) 가독성 향상
  2. 테스트 실패 메시지를 이해하기 쉬움
  3. 다양한 Matcher를 제공
  • Hamcrest의 Matcher를 사용해서 사람이 읽기 편한 자연스러운 Assertion 문장을 구성할 수 있으며, 실행 결과가 “failed”일 경우 역시 자연스러운 “failed” 메시지를 확인할 수 있기때문에 가독성이 상당히 높아집니다

Junit → Hamcrest

//JUnit Assertion  
assertEquals(expected, actual)

assertNotNull(currencyName, "should be not null");

assertThrows(NullPointerException.class, () -> getCryptoCurrency("XRP"));

// Hamcrest Assertion 하나의 영문장처럼 표현가능 -> 가독성 up;
assertThat(actual, is(equalTo(expected)));

assertThat(currencyName, is(notNullValue()));

// Hamcrest 만으로 Assertion을 구성하기 힘들기 때문에 JUnit의 assertThrows() 메서드를 
//이용해서 assertThrows()의 리턴 값으로 전달 받은 Exception 내부의 정보를 가져와서
Throwable actualException = assertThrows(NullPointerException.class,
                () -> getCryptoCurrency("XRP"));
// 아래처럼 추가로 검증해야 한다.
assertThat(actualException.getCause(), is(equalTo(null)));

슬라이스 테스트

API 계층 - Controller 테스트

@SpringBootTest       
// Spring Boot 기반의 애플리케이션을 테스트 하기 위한 Application Context를 생성
//애플리케이션에 필요한 Bean 객체들이 등록되어 있다.
@AutoConfigureMockMvc  
// Controller 테스트를 위한 애플리케이션의 자동 구성 작업을 해준다.
public class ControllerTestDefaultStructure {
		// DI로 주입바은 MockMvc는 Tomcat 서버 실행 없이 테스트할 수 있는 완벽한 환경을 지원
		// 해주는  일종의 Spring MVC 테스트 프레임워크이다. MockMvc 객체를 통해 우리가 작성한       Controller를 호출

    @Autowired
    private MockMvc mockMvc;

		// (4)
    @Test
    public void postMemberTest() {
        // given (5) 테스트용 request body 생성

        // when (6) MockMvc 객체로 요청 URI, HTTP 메서드 지정하고, 5에서 만든
				//          requset body추가하여 request 진행

ResultActions actions =
                mockMvc.perform(//Controller의 핸들러 메서드에 요청을 전송하기 
																//perform 메서드 호출
																//MockMvcRequestBuilders 클래스를 이용해서 빌더 패턴을 통해 request 정보 채워넣음
	                                post("/v11/members") 
                                        .accept(MediaType.APPLICATION_JSON)
                                        .contentType(MediaType.APPLICATION_JSON) 
                                        .content(content)   // request Body 데이터 설정
                                );

        // then Controller 핸들러 메서드에서 응답으로 수신한 HTTP Status 및 response body 검증

//ResultActions 객체를 이용해서 우리가 전송한 request에 대한 검증을 수행
MvcResult result = actions
//andExpect() 메서드를 통해 파라미터로 입력한 매처(Matcher)로 예상되는 기대 결과를 검증
                                .andExpect(status().isCreated()) 
//jsonPath()를 사용하면 JSON 형식의 개별 프로퍼티에 손쉽게 접근
															andExpect(jsonPath("$.data.email").value(post.getEmail()))
//andReturn()을 통해서 response 데이터를 확인 디버깅 용도
                                .andReturn(); 
// System.out.println(result.getResponse().getContentAsString());
    }
}

//response body로 전달된 JSON 데이터에서 한글이 깨져 보일 경우, application.yml 파일에 아래의 설정을 추가

server:
  servlet:
    encoding:
      force-response: true

@WebMvcTest

  • 애너테이션을 사용할 경우, Controller에서 의존하는 컴포넌트들을 모두 일일이 설정해 주어야 하는 불편함이 있습니다.

**데이터 액세스 계층 테스트**

  • 데이터 액세스 계층 테스트 시에는 DB의 상태를 테스트 케이스 실행 이전으로 되돌려서 깨끗하게 만들어야 한다.
  • @DataJpaTest 애너테이션을 테스트클래스에 추가하면, Repository 기능을 사용하기 위한 설정 Spring이 자동으로 해준다.
    • @Transactional애너테이션을 포함하고 있다.
    • 하나의 테스트 케이스 실행이 종료되는 시점에 데이터베이스에 저장된 데이터는 rollback 처리

**Mockito**

**Mock이란?**

목업(Mock-up)

  • 실제 제품이 나오기 전에 내부적으로 사용하기 위한 모형(또는 가짜)제품

Mock - 가짜 객체

Mocking

  • 단위 테스트나 슬라이스 테스트 등에 Mock 객체를 사용하는 것

**Mock 객체를 사용하는 이유**

  • 다른 계층과 단절시켜 불필요한 과정을 줄이고 테스트 대상에만 집중할 수있다.

**Mockito란?**

  • Mock 객체를 생성하고, 해당 Mock 객체가 진짜처럼 동작하게 하는 기능을 하는 Mocking framework
@SpringBootTest
@AutoConfigureMockMvc
class MemberControllerMockTest {
    ...

    // Application Context에 등록되어 있는 Bean에 대한 Mock객체를 생성하고 주입해주는 역할
    @MockBean
    private MemberService memberService;

    @Autowired
    private MemberMapper mapper;

    @Test
    void postMemberTest() throws Exception {
        // given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com",
                                                        "홍길동",
                                                    "010-1234-5678");
				
        Member member = mapper.memberPostToMember(post);
        member.setStamp(new Stamp());    

        // Mockito에서 지원하는 Stubbing 메서드
				// Mock 객체가 특정 값을 리턴하는 동작을 지정하는데 사용
        given(memberService.createMember
             (Mockito.any(Member.class))) // Mockito에서 지원하는 변수 타입 중 하나

//   Mock 객체 MemberService의 createMember() 메서드가 리턴 할 Stub 데이터
                .willReturn(member);

        String content = gson.toJson(post);

        // when
        ResultActions actions =
                mockMvc.perform(
                                    post("/v11/members")
                                        .accept(MediaType.APPLICATION_JSON)
                                        .contentType(MediaType.APPLICATION_JSON)
                                        .content(content)
                                );

        // then
        MvcResult result = actions
                                .andExpect(status().isCreated())
                                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                                .andExpect(jsonPath("$.data.name").value(post.getName()))
                                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                                .andReturn();

//        System.out.println(result.getResponse().getContentAsString());
    }
}

Stubbing
Mock 객체가 항상 일정한 동작을 하도록 지정하는 것

  • Mock 객체MemberService 클래스는 우리가 테스트하고자 하는 Controller의 테스트에 집중할 수 있도록 다른 계층과의 연동을 끊어주는 역할을 한다.
  • Mockito를 잘 이용하면 의존하는 다른 메서드 호출이나 외부 서비스의 호출을 단절 시킬 수 있기 때문에 우리가 원하는 테스트의 범위를 최대한 좁힐 수 있다.

비즈니스계층 Mockito 적용

//Junit에서 Spring을 사용하지 않고 순수하게 Mockito의 기능만을 사용하기 위해서는
// @ExtendWith(MockitoExtension.class)를 추가해야 한다.

@ExtendWith(MockitoExtension.class)
public class MemberServiceMockTest {
    @Mock   // @Mock을 추가하면 해당 필드의 객체를 Mock객체로 생성
    private MemberRepository memberRepository;

    @InjectMocks    // @Mock 애너테이션을 통해 생성된 Mock 객체는 @InjectMocks 애너테이션            을 추가한 필드에 주입된다.
    private MemberService memberService;

    @Test
    public void createMemberTest() {
        // given
        Member member = new Member("hgd@gmail.com", "홍길동", "010-1111-1111");

        //Mock객체로 Stubbing
        given(memberRepository.findByEmail(Mockito.anyString()))
                .willReturn(Optional.of(member)); // (5)

				// when / then (6)
        assertThrows(BusinessLogicException.class, () -> memberService.createMember(member));
    }
}

**전통적인 개발 방식**

  1. 서비스 제작에 관여하는 이해 당사자(기획자, 프런트엔드 개발자, 백엔드 개발자, 웹 디자이너 등)가 모여 서비스에 대한 컨셉과 해당 컨셉에 따른 요구 사항을 지속적으로 수집
  2. 요구 사항에 맞춰 서비스를 화면으로 제공하기 위한 UI(User Interface)를 설계하면서 구체적인 기능 요구 사항들을 정의
  3. 프런트엔드 개발자는 기능 요구 사항과 UI를 통해 프런트엔드 측 개발을 진행하고, 웹 디자이너는 화면을 디자인 하며, 백엔드 개발자는 역시 기능 요구 사항에 맞춰 백엔드 애플리케이션을 디자인
    1. 요구 사항과 설계된 화면(UI 설계서 등) 등을 기반으로 도메인 모델을 도출
    2. 클라이언트의 요청을 받아들이는 엔드포인트와 비즈니스 로직, 데이터 액세스를 위한 클래스와 인터페이스 등을 설계해서 큰 그림 그려본다
    3. 클래스와 인터페이스의 큰 틀을 작성
    4. 클래스와 인터페이스 내에 메서드를 정의하면서 세부 동작을 고민하고, 코드로 구현
    5. 메서드의 기능 구현이 끝났다면 구현한 기능이 잘 동작하는지 테스트
    6. 테스트에 문제가 발생한다면 구현한 코드를 디버깅하면서 문제의 원인 찾기

TDD

  • 테스트구현하는 설계방식

**TDD의 특징**

  • TDD는 모든 조건에 만족하는 테스트를 먼저 진행한 뒤에 조건에 만족하지 않는 테스트를 단계적으로 진행하면서 실패하는 테스트를 점진적으로 성공시켜 갑니다.
  • TDD는 테스트 실행 결과가 “failed”인 테스트 케이스를 지속적으로 그리고 단계적으로 수정하면서 테스트 케이스 실행 결과가 “passed”가 되도록 만들고 있습니다.
  • TDD는 테스트가 “passed” 될 만큼의 코드만 우선 작성합니다.
  • TDD는 ‘실패하는 테스트 → 실패하는 테스트를 성공할 만큼의 기능 구현 → 성공하는 테스트 → 리팩토링 → 실패하는 테스트와 성공하는 테스트 확인’ 이라는 흐름을 반복합니다.

**장점**

  • 한번에 너무 많은 기능을 구현할 필요가 없다.
  • 단순한 기능에서 복잡한 기능으로 확장 되면서 검증을 빼먹지 않고 할수 있다.
  • 리팩토링 할 부분이 눈에 보이면 그때 그때 리팩토링을 빠르게 진행 → 리팩토링의 비용이 상대적으로 적어진다.
profile
백엔드 개발자

0개의 댓글