테스트 코드를 작성하는 다양한 방법

테스트 코드를 작성하는 다양한 방법

테스트 코드를 작성하는 다양한 방법

Spring Boot 2.2.X 버전 부터 기존에 사용하던 Junit4 가 아닌 Junit5가 기본 디펜던시로 추가 되었습니다.
Junit4를 사용하지 못하는건 아니지만 스프링이 Junit5를 완전히 지원한다는 이야기니 왠만하면 Junit5를 사용합시다.
테스트 코드를 작성하는 3가지 방법을 소개 하나 각 방법에 대하여 깊은 이해가 있지 않아 맛보기 식으로 공유해드리겠습니다.

우선 Junit5 에서는 @RunWith(SpringRunner.class) 라는 애노테이션이 필요 없어졌습니다.
대신 Extend 기능을 활용해서 필요한 디펜던시를 추가할 수 있습니다.

BeforeAll, BeforeEach 등의 애노테이션을 사용해서 테스트 메소드 실행전 필요한 객체를 생성 하도록 변경되었습니다.

주로 사용하는 기능

  • Setup 기능 : BeforeEach , BeforeAll 등의 애노테이션으로 테스트 시작 전 필요한 데이터를 설정 할 수 있음. IntelliJ 에서 지원.
    • BeforeAll 같은 경우 Junit4에서는 지원하지 않던 Static한 데이터 공유가 가능함.
  • DisplayName : 테스트의 이름을 표시 할 수 있는 기능.
  • ParameterizeTest : 테스트 실행시 필요한 파라미터를 전달하여 @ValueSource사용 실행이 가능함.
  • ActiveProfile : 테스트 프로퍼티를 별도로 관리할 경우 테스트 프로파일 설정.

그 외로 새롭게 사용할 수 있는 기능들은 Docs를 확인해주시길 바랍니다.
JUnit 5 User Guide

SpringBootTest 를 이용한 컨트롤러 통합 테스트

@SpringBootTest 애노테이션을 테스트 환경에서 사용할 시 SpringBoot Application이 구동되어 등록되어 있는 빈을 전부 로드하게 됩니다.
이로 인해서 테스트는 등록된 빈을 사용해서 모든 로직등 실제 사용하는 빈을 통하여 실행되게 됩니다.

통합 테스트

MockMvc 을 사용하기 위해서 @AutoConfigureMockMvc 애노테이션을 등록합니다.
MockMvc 객체는 테스트 코드에서 Http요청을 보낼수 있도록 도와주는 객체 입니다.

슬라이싱 테스트

  • @DataJpaTest : JpaRepository 테스트 시 사용
  • @EnableWebMvc : Controller 테스트

참고 : 테스트 코드에서 @Transaction애노테이션을 사용할 경우 기본 Rollback정책이 True이므로 Rollback하지 않는 데이터를 확인 하기 위해선 false로 지정 해줘야 합니다.

Model 검증

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
@DisplayName("index 페이지 정상 작동")
public void indexPage() throws Exception {
    ResultActions resultActions = mockMvc.perform(get("/index"))
            .andDo(print())
            .andExpect(status().isOk());
    MvcResult mvcResult = resultActions.andReturn();
    Map<String, Object> model = 	mvcResult.getModelAndView().getModel();
    assertThat(model.get("facebookAppId")).isNotNull();
    assertThat(model.get("kakaoAppId")).isNotNull();
    assertThat(model.get("s3Uri")).isNotNull();
    assertThat(model.get("tagManagerCode")).isNotNull();
    assertThat(model.get("host")).isNotNull();
}

API 검증

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
@DisplayName("상품 정상 생성")
void create() throws Exception {
    String productName = "간장치킨";
    BigDecimal price = BigDecimal.valueOf(17000);

    Product product = createProduct(productName, price);

    mockMvc.perform(post("/api/products")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(product)))
            .andDo(print())
            .andExpect(status().isCreated())
            .andExpect(header().exists(HttpHeaders.LOCATION))
            .andExpect(jsonPath("id").exists())
            .andExpect(jsonPath("name").value(productName))
            .andExpect(jsonPath("price").value("17000.0"))
            ;
}

Mockito 를 이용한 단위 테스트

특정 객체를 Mocking하여 단위 테스트를 하기 위해선 일반적으로 Mockito를 활용하여 테스트 합니다.
Mocking을 하는 이유는 개인적으로는 단위 테스트를 정확하게 하고자 함에 있다고 생각합니다.
여기서 말하는 정확한 단위 테스트는 예를 들면 Service Layer를 테스트 할때 Dao (Repository) 에서 발생하는 이슈가 테스트에 영향을 끼치지 않게 함에 있습니다.
Service Logic 을 정확히 테스트 하고 Repository는 따로 Repository 테스트를 만들어서 테스트 함을 권장하고 있습니다.

Mocking을 통한 테스트를 작성하려면 @ExtendWith(MockitoExtension.class) 애노테이션을 사용하면 됩니다.

1
2
3
4
5
@Mock // 객체 Mocking
private MenuGroupDao menuGroupDao;

@InjectMocks // Mock 객체 주입
private MenuGroupBo menuGroupBo;

위 코드와 같이 Mock객체를 주입 받아 테스트를 실행할 객체를 설정합니다.
Mocking 된 객체는 Stubbing을 통하여 객체의 행동을 조작할 수 있습니다.

1
2
given(menuGroupDao.findAll()).willReturn(menuGroups); // BDD style
when(menuGroupDao.findAll()).thenReturn(menuGroups);

given또는 when ~ 메서드를 사용하여 객체의 행동을 조작합니다.
조작한 행동을 통하여 원하는 결과가 나오는지 확인을 할 수 있습니다.

샘플코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@DisplayName("테이블 착석 상태를 비움으로 변경")
@Test
void changeEmpty() {
    OrderTable requestOrderTable = new OrderTableBuilder()
            .setEmpty(true)
            .build()
            ;

    OrderTable orderTable = new OrderTableBuilder()
            .setEmpty(false)
            .setId(1L)
            .build()
            ;

    when(orderTableDao.findById(orderTable.getId())).thenReturn(Optional.of(orderTable));
    when(orderDao.existsByOrderTableIdAndOrderStatusIn(orderTable.getId(),
            Arrays.asList(OrderStatus.COOKING.name(), OrderStatus.MEAL.name()))).thenReturn(false);
    when(orderTableDao.save(orderTable)).thenReturn(orderTable);

    OrderTable changedOrderTable = tableBo.changeEmpty(orderTable.getId(), requestOrderTable);

    assertThat(changedOrderTable.isEmpty()).isTrue();
}


많은 기능이 존재함으로 역시나 문서를 확인 합니다.
Mockito (Mockito 3.2.4 API)

Fake Object 를 이용한 단위 테스트

Fake Object 라는 개념이 혼란 스러울수 있습니다.
이 개념은 Mocking 과 비슷하면서도 다른 개념이라 아직 제대로 이해하지 못하였습니다.

Fake Object 를 사용하는 이유에 대하여 설명드리자면 Mocking을 통한 테스트는 실제 로직이 어떻게 돌아가는지 다 알고 있는 상태여야 합니다. 그래서 원하는 결과값을 (행위에 대한 결과)를 받을수 있도록 Stubbing 합니다. 일명 Whitelist Test 라고 할 수 있습니다.
반면에 Fake Object를 사용하면 어떤 로직이 실행되는지 전혀 알 필요가 없고 가짜 객체를 통한 실행 결과만 받을 수 있으면 테스트를 작성 할 수 있게 됩니다.

샘플코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private ProductDao productDao = new FakeProductDao();

private ProductBo productBo;

@BeforeEach
void setUp() {
    productBo = new ProductBo(productDao);
}

@DisplayName("상품을 등록 할 수 있다")
@Test
void create() {
    //given
    Product expected = new Product();
    expected.setId(1L);
    expected.setName("치킨");
    expected.setPrice(BigDecimal.valueOf(16_000L));

    //when
    Product actual = productBo.create(expected);

    //then
    assertThat(actual).isNotNull();
    assertThat(actual.getName()).isEqualTo(expected.getName());
    assertThat(actual.getPrice()).isEqualTo(expected.getPrice());
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FakeProductDao implements ProductDao {
    private Map<Long, Product> entities = new HashMap<>();

    @Override
    public Product save(Product entity) {
        entities.put(entity.getId(), entity);
        return null;
    }

    @Override
    public Optional<Product> findById(Long id) {
        return Optional.ofNullable(entities.get(id));
    }

    @Override
    public List<Product> findAll() {
        return new ArrayList<>(entities.values());
    }
}