관리 메뉴

ballqs 님의 블로그

[Spring] Test 코드 작성(stub , mock) 본문

코딩 공부/Spring

[Spring] Test 코드 작성(stub , mock)

ballqs 2024. 8. 28. 15:17

오늘은 강의를 들으면서 테스트 코드 작성하는 것에 대해서 메모했다.

 

기본적인 테스트 코드 작성 방법

MyMath.java 코드 작성

public class MyMath {

    public int calculateSum(int[] numbers) {
        int sum = 0;

        for (int number : numbers) {
            sum += number;
        }

        return sum;
    }
}

 

테스트 코드 작성

public class MyMathTest {

    private MyMath math = new MyMath();

    @Test
    void calculateSum_ThreeMemberArray() {
        // Absence of failure is success
        // Test Condition or Assert
        assertEquals(6 , math.calculateSum(new int[] {1,2,3}));
    }
}

실행시켜보면 테스트가 성공한 것을 알수 있다.

 

여기서 assert 란?

인수를 검증하고 조건에 맞지 않은 경우 IllegalArgumentException 또는 IllegalStateException를 발생시킨다.

이 부분은 조건문을 단순화하고 반복적인 코드를 줄이는 역할을 한다.

 

메소드 종류

Method 설명
assertEquals(x , y) x와 y의 값이 일치한지 확인한다.
assertArrayEquals(a , b) 배열 a와 b가 같은지 확인한다.
assertFalse(x) x가 false인지 확인한다.
assertTrue(x) x가 true인지 확인한다.
assertTrue(message , condition) condition이 true이면 message 표시
assertNull(o) o가 null인지 확인한다.
assertNotNull(o) o가 null이 아닌지 확인한다.
assertSame(ox , oy) ox와 oy가 같은 객체인지 확인한다.
assertNotSame(ox , oy) ox와 oy가 같은 객체가 아닌지 확인한다.
assertfail() 테스트 실패처리

 

Test 코드의 Annotation종류

Annotataion 설명
@BeforeAll 모든 테스트가 시작하기 전에 실행
@AfterAll 모든 테스트가 끝나고 나서 실행
@BeforeEach 테스트별 실행 전에 실행
@AfterEach 테스트별 실행이 끝나고 실행

 

사용 예제

public class MyMathTest {

    private MyMath math = new MyMath();

    @BeforeEach
    void beforeEach() {
        // 테스트별 시작전
        System.out.println("Before each");
    }

    @AfterEach
    void afterEach() {
        // 테스트별 종료후
        System.out.println("After each");
    }

    @BeforeAll
    static void beforeAll() {
        // 모든 테스트의 시작 전
        // 클래스 레벨 메서드이기에 static를 붙여야함
        System.out.println("Before all");
    }

    @AfterAll
    static void afterAll() {
        // 모든 테스트가 끝나고 나서
        // 클래스 레벨 메서드이기에 static를 붙여야함
        System.out.println("After all");
    }

    @Test
    void calculateSum_ThreeMemberArray() {
        System.out.println("calculateSum_ThreeMemberArray 실행");
        assertEquals(6 , math.calculateSum(new int[] {1,2,3}));
    }

    @Test
    void calculateSum_ZeroMemberArray() {
        System.out.println("calculateSum_ZeroMemberArray 실행");
        assertEquals(0 , math.calculateSum(new int[] {}));
    }

    @Test
    void test() {
        System.out.println("test 실행");
        assertArrayEquals(new int[] {1,2} , new int[] {1,2});
    }
}

 


Stub이란?

인스턴스화하여 구현한 가짜 객체(Dummy, 기능 구현 x)를 이용해 실제로 동작하는 것처럼 보이게 만드는 객체

해당 인터페이스나 클래스를 최소한으로 구현하여 테스트에서 호출된 요청에 대해 미리 준비해둔 답변을 응답하는 것

 

DataService.java 작성

interface DataService {
    int[] retrieveAllData();
}

 

SomeBusinessImpl.java 작성

public class SomeBusinessImpl {

    private DataService dataService;

    SomeBusinessImpl(DataService dataService) {
        super();
        this.dataService = dataService;
    }

    public int findTheGreatestFromAllData() {
        int[] data = dataService.retrieveAllData();
        int greatestValue = Integer.MIN_VALUE;
        for (int value : data) {
            if (value > greatestValue) {
                greatestValue = value;
            }
        }
        return greatestValue;
    }
}

 

여기서 보면 retrieveAllData 메서드가 아직 미구현이라는 것을 알수 있다.

 

stub를 이용하여 테스트 하기 위해

stub 코드를 작성

class DataServiceStub1 implements DataService {
    @Override
    public int[] retrieveAllData() {
        return new int[]{25 , 15 , 5};
    }
}

 

테스트 코드 작성

public class SomeBusinessImplStubTest {
    @Test
    void test() {
        DataServiceStub1 dataServiceStub = new DataServiceStub1();
        SomeBusinessImpl businessImpl = new SomeBusinessImpl(dataServiceStub);
        int result = businessImpl.findTheGreatestFromAllData();
        assertEquals(25 , result);
    }
}

 

여기서 stub의 문제점이 생긴다.

  1. DataService에 새로운 기능이 추가될때마다 DataServiceStub1에도 새로 업데이트 해줘야 하는 문제점
  2. stub을 이용하면 많은 시나리오 테스트하기가 어려운 점
  3. 추가적인 시나리오를 만들고 싶다 생각하면 똑같이 만들어서 업데이트 해줘야하는 점

이런 문제점을 해결하고자 mock에 대해서 알아보았다.


Mock 이란?

호출에 대한 기대를 명세하고, 내용에 따라 동작하도록 프로그래밍된 객체

 

DataService.java 작성

interface DataService {
    int[] retrieveAllData();
}

 

SomeBusinessImpl.java 작성

public class SomeBusinessImpl {

    private DataService dataService;

    SomeBusinessImpl(DataService dataService) {
        super();
        this.dataService = dataService;
    }

    public int findTheGreatestFromAllData() {
        int[] data = dataService.retrieveAllData();
        int greatestValue = Integer.MIN_VALUE;
        for (int value : data) {
            if (value > greatestValue) {
                greatestValue = value;
            }
        }
        return greatestValue;
    }
}

 

 

Annotataion 없이 작성

public class SomeBusinessImplMockTest {
    @Test
    void findTheGreatestFromAllData_basicScenario() {
        // mock
        DataService dataServiceMock = mock(DataService.class);
        // 해당 메서드에서 thenReturn이라는 결과값을 리턴받는 것으로 가정함
        when(dataServiceMock.retrieveAllData()).thenReturn(new int[] {25, 15 , 5});
        SomeBusinessImpl businessImpl = new SomeBusinessImpl(dataServiceMock);
        // 위의 리턴값을 바탕으로 밑의 메서드 행위를 검증함
        int result = businessImpl.findTheGreatestFromAllData();
        System.out.println(result);
        // 예상하는 값은 25이며 결과값 또한 25로 테스트 통과
        assertEquals(25 , result);
    }
}

 

Annotatation 으로 작성

@ExtendWith(MockitoExtension.class)
public class SomeBusinessImplMockTest {

    // mock지정
    @Mock
    private DataService dataServiceMock;

    // mock이 적용되고 행위검증할 클래스가 무엇인지 정하는 것으로 추정
    @InjectMocks
    private SomeBusinessImpl businessImpl;

    @Test
    void anotationTest() {
        // anotataion으로 Test하는 방법
        when(dataServiceMock.retrieveAllData()).thenReturn(new int[] {25, 15 , 5});
        assertEquals(25 , businessImpl.findTheGreatestFromAllData());
    }
}

 


Stub과 Mock의 차이

stub은 상태 검증(state verification) 을 사용하고 Mock 오브젝트는 행위 검증(behavior verification) 사용한다.

상태 검증 : 메서드가 수행된 후, 객체의 상태를 확인해 올바르게 동작했는지 확인하는 검증법

행위 검증 : 메소드의 리턴 값으로 판단 할 수 없는 경우 , 특정 동작을 수행하는지 확인하는 검증법