1. 소프트웨어 테스팅
이번 시간에는 스프링 테스트에 대해서 알아보겠다.
- 위키백과의 소프트웨어 테스트를 읽어보자.
- 소프트웨어의 결함이 있는지 찾는 것이다.
- 테스팅은 각각의 절차가 있다.
Testing Pyramid
테스트 자동화시에 어떻게 layer별로 테스트를 작동했는가.
단위 테스트
백엔드는 단위 테스트가 중요하다. 제일 많이 한다.
단위 테스트는 자동화 테스트를 위한 것이다.
- 하나의 클래스를 테스트하는 것이다.
- 특정 부분을 고립해서 테스트한다.
- SUT : System Under Test(테스트하는 대상이 된다. )
- 의존관계를 맺고 있는 클래스는 테스트 더블이라는 클래스로 전달한다.(가짜)
- 새로 추가한 부분이 기존의 것을 잘 커버하고 있는가
- 테스트 케이스만 봐도 기능을 유추할 수 있다.
통합 테스트 Integration Test
: 테스트 클래스가 다른 의존 클래스들과 연동이 잘 되는가
외부 시스템의 연동 테스트도 포함이 된다.
- 시스템 전체를 보는 것을 end-to-end test라고 한다. 처음부터 끝까지 다 테스트
- functional test가 end-to-end test이다.
JUnit
자바에서 실제로 test code를 작성하는 법을 배운다.
가장 많이 사용하는 오픈소스 프레임워크다.
현재까지 버전 5가 최신 버전이다.
- 매 단위 테스트마다 클래스의 인스턴스가 생성되어 독립적인 테스트가 가능하게 한다.
- 어노테이션을 제공하여 테스트의 라이프 사이클을 관리한다. @BeforeAll @AfterEach 등
- assert 제공
- 인텔리제이의 create Test를 이용하여 쉽게 만들 수 있다.
- setUp/ @Before
- 테스트 전 해야하는 일이 있는 경우 - tearDown/ @After
- 마지막에 리소스를 정리하던가 clean up을 해야 한다.
// FixedAmountVoucherTest.java
import static org.junit.jupiter.api.Assertions.*;
class FixedAmountVoucherTest {
@Test
@DisplayName("기본적인 assertEqual 테스트") // 이모지도 넣을 수 있다.
void testAssertEqual(){
assertEquals(2, 1+1);
}
@Test
@DisplayName("주어진 금액만큼 할인을 해야 한다")
void testDiscount() {
var sut = new FixedAmountVoucher(UUID.random(), 100);
assertEquals(900, sut.discount(1000));
}
}
- 테스트 코드는 어떠한 것도 return 하면 안된다. void
- 얘를 어떻게 하면 망가뜨릴까
// FixedAmountVoucherTest.java
...
@Test
@DisplayName("할인 금액은 마이너스가 될 수 없다.")
void testWithDinus() {
assertThrows(IllegalArgumentException.class, () => new FixedAmountVoucher(UUID.random(), -100));
}
만약 잠깐 테스트를 동작하지 않게 하고 싶으면 @Disabled 사용
// FixedAmountVoucherTest.java
...
@BeforeAll
static void setup() {
// 단 한번 실행
}
@BeforeEach
void init() {
// 매 테스트 마다 한번 실행
}
테스트 코드를 작성하면서 미처 생각하지 못하는 것을 발견해야 한다.
- 큰 금액을 입력할때?
- 0을 입력해도 될까?
- 그룹화 해서 테스트 코드를 작성할 수 있다.
// FixedAmountVoucherTest.java
...
@Test
@DisplayName("유효한 할인 금액으로만 생성할 수 있다.")
void testVoucherCreation() {
assertAll("FixedAmountVoucher creation"
() -> assertThrows(IllegalArgumentException.class, () 0 -> new FixedAmountVoucher(UUID.randomUUID(), 0));
() -> assertThrows(IllegalArgumentException.class, () 0 -> new FixedAmountVoucher(UUID.randomUUID(), -100));
() -> assertThrows(IllegalArgumentException.class, () 0 -> new FixedAmountVoucher(UUID.randomUUID(), 10000000));
);
}
테스트 코드를 작성하는 과정에서 코드 보완을 하는 건가보다..
- 구현 -> 테스트 코드
- 테스트 코드 -> 구현
자기 스타일대로 코드를 구현하자. edge case들이 존재하는구나!
이것만 보더라도 비지니스 룰을 파악할 수 있다.
// HamcrestAssertionTests.java
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class HamcrestAssertionTests {
@Test
@DisplayName("여러 hamcrest matcher 테스트")
void hamcrestTest() {
assertEquals(2, 1 + 1);
assertThat(1 + 1, equalTo(2));
assertThat(1 + 1, is(2));
assertThat(1 + 1, anyof(is(2), is(1)));
}
}
hamcrest는 리스트나 Collection에 대해서도 잘 사용할 수 있다.
// HamcrestAssertionTests.java
@Test
@DisplayName("컬렉션에 대한 matcher 테스트")
void hamcrestListMatcherTest() {
var prices = List.of(1, 2, 3);
assertThat(prices, hasSize(3));
assertThat(prices, everyItem(greatherThan(0)));
assertThat(prices, containsIsAnyOrder(3, 4, 2));
assertThat(prices, hasItem(3));
}
- assertThat 이 읽기 쉽기 때문에 쓰는 것을 선호한다.
Mock Object 모의 객체
Test double : 의존 클래스를 대신하는 객체
- Test double 에는 여러 개의 종류가 있다.
- Mock(mock, spy)
- Stub(stub, dummy, fake)
Stub이 가짜 개체다. Mock은 호출에 대한 기대를 명시한다.
- Mock 은 행위에 대해 집중한다.
- Stub 은 실제 동작하는 것 처럼 보이게 만든다.
자바에서는 Mockito라는 것을 가장 많이 사용한다. Order Test 코드를 작성해보자.
// OrderServiceTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
class OrderServiceTest {
class OrderRepositoryStub implements OrderRepository {
@Override
public Order insert(Order order) {
return null;
}
}
@Test
void createOrder() {
// Given
var voucherRepository = new MemoryVoucherRepository();
var fixedAmountVoucher = new FixedAmountVoucher(UUID.randomUUID(), 100);
voucherRepository.insert(fixedAmountVoucher);
var sut = new OrderService(new VoucherService(voucherRepository), new OrderRepositoryStub());
// When
var order = sut.createOrder(UUID.randomUUID(), List.of(UUID.randomUUID(), 200, 1)), fixedAmountVoucher.getVoucherId());
// Then
assertThat(order.totalAmount(), is(100L));
assertThat(order.getVoucher().isEmpty(), is(false));
assertThat(order.getVoucher().get().getVoucherId(), is(fixedAmountVoucher.getVoucherId()));
assertThat(order.getOrderStatus(), is(OrderStatus.ACCEPTED));
}
}
- 상태에 집중하냐 행위에 집중하냐 선택해야 한다.
// OrderServiceTest.java
import static org.mockito.Mockito.mock;
class OrderServiceTest {
...
@Test
@Displayname("오더가 생성되어야 한다 (mock)")
void createOrderByMock() {
// Given
var voucherService = mock(VoucherService.class);
var orderRepository = mock(OrderRepository.class);
var fixedAmountVoucher = new FixedAmountVoucher(UUID.randomUUID(), 100);
when(voucherService.getVoucher(fixedAmountVoucher.getVoucherId())).thenReturn(fixedAmountVoucher);
var sut = new OrderService(voucherService, orderRepository);
// When
ver order = sut.createOrder(
//
);
// Then
verify(voucherServiceMock).getVoucher(fixedAmountVoucher.getVoucherId());
verify(orderRepositoryMock).insert(order);
verify(voucherServiceMock).useVoucher(fixedAmountVoucher);
}
}
- mock는 행위에 집중한다. 어떤 메소드가 만들어지는지 정의해줘야 한다.
- 정의를 한 것만 리턴되게 기술해야 한다.
- 어떤 메소드가 정상적으로 호출되어지는지(행위 관점)
- inorder를 사용한다면 어떤 순서에 의해서 호출되어져야 하는지 판단할 수 있다.
var inOrder = inOrder(voucherServiceMock);
inOrder.verify(voucherServiceMock).useVoucher(fixedAmountVoucher);
inOrder.verify(voucherServiceMock).getVoucher(fixedAmountVoucher.getVoucherId());
verify(voucherServiceMock).getVoucher(fixedAmountVoucher.getVoucherId()); verify(orderRepositoryMock).insert(order);
- inOrder로 순서를 파악할 수 있다(get -> insert -> use)
Spring의 JUnit 5 지원
- MOCK 오브젝트를 만들기 어려운 테스트를 지원한다.
'2023 활동 - 4학년 > [1월 ~ 4월] sw 아카데미 백엔드 과정' 카테고리의 다른 글
[2023.03.09 / CNU SW 아카데미] SpringBoot Part2 D-23 (0) | 2023.03.09 |
---|---|
[2023.03.08 / CNU SW 아카데미] SpringBoot Part2 D-22 (0) | 2023.03.08 |
[2023.03.03 / CNU SW 아카데미] 42일차 회고록 (0) | 2023.03.05 |
[2023.03.02 / CNU SW 아카데미] SpringBasic Part1 D-20 (0) | 2023.03.02 |
[2023.03.02 / CNU SW 아카데미] 41일차 회고록 (0) | 2023.03.02 |