본문 바로가기
2023 활동 - 4학년/[1월 ~ 4월] sw 아카데미 백엔드 과정

[2023.03.08 / CNU SW 아카데미] SpringBoot Part2 D-21

by 은행장 노씨 2023. 3. 8.

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 오브젝트를 만들기 어려운 테스트를 지원한다.