JVM/JUnit

[JUnit] 소개 - 4. Mokitio 란 ?

헹창 2023. 9. 26.
반응형

Mockito

Mockito 란 ?

  • 복잡하게 얽혀있는, 의존성을 가지는 객체들을 가짜 객체로 만들어 테스트할 수 있는데 이런 객체를 Mock 객체라고 한다. (참고)
  • Mockito는 관리가 어려운 Mock 객체를 손쉽게 사용하도록 지원해주는 프레임워크다.

Mockito 단계

  • Mock 객체 생성
  • Mock 객체 행동 (Stubbing) : when 메서드를 통해 원하는 동작을 미리 정하고 이를 기반으로 테스트한다.
  • Mock 작동 검증 (Verify) : 호출 등이 정확히 됐는지 확인한다.

Mock 생성 방식

  • mock Method
import static org.mockito.Mockito.mock;

public class MockStudy {

	private UserService userService;

	@Test
	public void test() {
		userService = mock(UserService.class);
		// ...
	}

}
  • @Mock annotation
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
public class MockStudy {

	@Mock
	private UserService userService;

	@Test
	public void test() {
		// ...
	}

}

 

Stubbing (Mock 객체 행동)

  • when : Mock 객체의 행동을 조작하는 메소드
  • mock을 사용하고, stubbing 해주지 않으면 reference 타입은 Null, Primtive 타입은 0을 반환함

Example

  • UserService
public class UserService {

	public User getUser() {
		return new User("haenny", 28);	// name, age
	}

	public int getLoginErrNum() {
		return 1;	
	}
    
}
  • Test Code @Mock
@ExtendWith(MockitoExtension.class)
public class MockStudy {

	@Mock
	UserService userService;

	@Test
	void testReferenceType() {
		assertNull(userService.getUser());	// Test Success
	}

	@Test
	void testPrimitiveType() {
		assertEquals(0, userService.getLoginErrNum());	// Test Success
	}

	@Test
	void testStubing() {
		User user = new User("heng", 28);
		when(userService.getUser()).thenReturn(user);
	
		assertEquals("haenny", userService.getUser().getName());	// Test Fail
	}
    
}

 

Stubbing(스터빙) 방법

1. OngoingStubbing method

when({stubbing method}).{ongoinStubbing method}
  • when 메소드의 리턴 값을 정의해주는 메소드
thenReturn 스터빙한 메소드 호출 후 어떤 객체를 리턴할 지 정의
thenThrow 스터빙한 메소드 호출 후 어떤 Exception Throw할 지 정의
thenAnswer 스터빙한 메소드 호출 후 어떤 작업을 할 지 커스텀 정의
- thenReturn, thenTrhow 사용 권장
thenCallRealMethod 실제 메소드 호출

Example

  • Service
public class ProductService {
    public Product getProduct() {
        return new Product("A001", "monitor");
    }
    public Product getProduct(String serial, String name) {
        return new Product(serial, name);
    }
}
  • Test Code (OngoingStubbing method)
@ExtendWith(MockitoExtension.class)
public class OngoingStubbingMethod {
  
    @Mock
    ProductService productService;
  
    @Test
    void testThenReturn() {
        Product product = new Product("T001", "mouse");  
        when(productService.getProduct()).thenReturn(product);  
        assertThat(productService.getProduct()).isEqualTo(product);	
    }
  
    @Test
    void testThenThrows() {
        when(productService.getProduct()).thenThrow(new IllegalArgumentException());  
        assertThatThrownBy(() -> productService.getProduct()).isInstanceOf(IllegalArgumentException.class);
    }
  
    @Test
    void testThenAnswer() {
        when(productService.getProduct(any(), any())).thenAnswer((Answer) invocation -> {
            Object[] args = invocation.getArguments();  
            return new Product(args[0] + "1", args[1] + "_1233");
        });
  
        assertThat(productService.getProduct("S001","desk").getSerial()).isEqualTo("S0011");
    }
  
    @Test
    void testThenCallRealMethod() {
        when(productService.getProduct()).thenCallRealMethod();
  
        assertThat(productService.getProduct().getSerial()).isEqualTo("A001");
    }
}
  • 메소드 호출마다 바뀌는 스터빙 방법
    : OngoingStubbing 메소드를 메소드 체이닝으로 사용할 시 메소드 호출마다 다른 스터빙을 호출할 수 있음
@ExtendWith(MockitoExtension.class)
public class ConsecutiveStubbing {
  
    @Mock
    ProductService productService;
  
    @Test
    void testConsecutiveStubbing() {
        Product product = new Product("D001","water");
  
        when(productService.getProduct())
                .thenReturn(product)
                .thenThrow(new RuntimeException());
  
        //첫 번째 호출 : .thenReturn(product)
        assertThat(productService.getProduct()).isEqualTo(product);
  
        //두 번째 호출 : .thenThrow(new RuntimeException());
        assertThatThrownBy(() -> productService.getProduct()).isInstanceOf(RuntimeException.class);
    }
}

2. Stubber method

{stubber method}.when({스터빙할 클래스}).{스터빙할 메소드}
  • 스터빙이 반드시 실행되어야 하는 경우 사용하는 메소드로, OngoingStubbing과 다르게 when에 스터빙할 클래스를 넣고 그 후에 메소드를 호출
doReturn 스터빙 메소드 호출 후 어떤 행동을 할 지 정의
doThrow 스터빙 메소드 호출 후 어떤 Exception을 throw할 지 정의
doAnswer 스터빙 메소드 호출 후 어떤 작업을 할 지, 커스텀하게 정의
doNothing 스터빙 메소드 호출 후 어떤 행동도 하지 않도록 정의

Example

// 예1
List list = new LinkedList();
List spy = spy(list);
//불가능 : list가 빈 객체이기 때문에 .get(0)을 할 때 IndexOutOfBoundsException을 발생하여 foo를 리턴하지 못 한다.
when(spy.get(0)).thenReturn("foo");
//위와 같은 경우를 doReturn을 사용한다.
doReturn("foo").when(spy).get(0);  
  
// 예2
when(mock.foo()).thenThrow(new RuntimeException());
//불가능 : 이미 mock.foo()를 호출할 때 RuntimeException이 일어나기 때문에 bar를 리턴할 수 없음
when(mock.foo()).thenReturn("bar");
//위와 같은 경우를 doReturn을 사용한다.
doReturn("bar").when(mock).foo();

void method 테스트 가능

  • when method 는 리턴 타입 인자로 T를 받기 때문에 void 메소드에 사용할 수 없었음
  • Stubber method로 가능
public class UserService {

	public User getUser() {
        return new User("haenny", 28);	// name, age
    }

	public int getLoginErrNum() {
        return 1;	
    }

    public void deleteUser() {}

}

// -------------------------------------------------------------------------
// Stubber Test Code
@ExtendWith(MockitoExtension.class)
public class StubberMethod {
  
    @Mock
    UserService userService;
  
    @Test
    void testDoReturn() {
        User user = new User("badguy", "4312");  
        doReturn(user).when(userService).getUser();  
        assertThat(userService.getUser()).isEqualTo(user);
    }
  
    @Test
    void testDoThrow() {
        doThrow(new RuntimeException()).when(userService).deleteUser();
        assertThatThrownBy(() -> userService.deleteUser()).isInstanceOf(RuntimeException.class);
    }
}

 

 

Verify (Mock 작동 검증)

  • 스터빙한 메소드 검증 방법 (Mock 작동 확인)
  • verify 메소드를 이용해 스터빙한 메소드의 실행여부/N번 실행여부/실행 초과 여부 등 다양하게 검증 가능
verify(T mock, VerificationMode mode)   // VerificationMode : 검증할 값을 정의하는 메소드

Method

메소드명 설명 (테스트 내에서 ~)
times(n) 몇 번이 호출됐는지 검증
never 한 번도 호출되지 않았는지 검증
atLeastOne 최소 한 번은 호출됐는지 검증
atLeast(n) 최소 n 번이 호출됐는지 검증
atMostOnce 최대 한 번이 호출됐는지 검증
atMost(n) 최대 n 번이 호출됐는지 검증
calls(n) n번이 호출됐는지 검증 (InOrder랑 같이 사용해야 함)
only 해당 검증 메소드만 실행됐는지 검증
timeout(long mills) n ms 이상 걸리면 Fail 그리고 바로 검증 종료
after(long mills) n ms 이상 걸리는지 확인

timeout과 다르게 시간이 지나도 바로 검증 종료가 되지 않는다.
description 실패한 경우 나올 문구

Example (verify method)

@ExtendWith(MockitoExtension.class)
public class VerifyMethod {
  
    @Mock
    UserService userService;
  
    @Test
    void testVerifyTimes() {
        userService.getUser();
        userService.getUser();
  
        verify(userService, times(2)).getUser();
    }
  
    @Test
    void testVerifyNever() {
        verify(userService, never()).getUser();
    }
  
    @Test
    void testAtLeastOne() {
        userService.getUser();
  
        verify(userService, atLeastOnce()).getUser();
    }
  
    @Test
    void testAtLeast() {
        //한 번만 실행하면 fail
        userService.getUser();
        userService.getUser();
        userService.getUser();
  
        verify(userService, atLeast(2)).getUser();
    }
  
    @Test
    void testAtMostOnce() {
        userService.getUser();
        // userService.getUser(); - 2번 이상 실행하면 fail
  
        verify(userService, atMostOnce()).getUser();
    }
  
    @Test
    void testAtMost() {
        userService.getUser();
        userService.getUser();
        userService.getUser();
        // userService.getUser(); - 6번 이상 실행하면 fail
  
        verify(userService, atMost(3)).getUser();
    }
  
    @Test
    void testOnly() {
        userService.getUser();
        // userService.getLoginErrNum(); - getUser()가 아닌 다른 메소드 실행 시 fail
  
        verify(userService, only()).getUser();
    }
}

Example (Method 호출 순서 검증)

  • 메소드 호출 검증을 위해 InOrder 사용
  • InOrder("Mock 객체 명")으로 생성 후 검증하고 싶은 순서에 맞게 verify 사용
    • verifyNoMoreInteractions(T mock) : 선언한 verify 후 해당 mock 실행 시 fail
    • verifyNoInteractions(T mock) : 테스트 내에서 mock 실행 시 fail
@ExtendWith(MockitoExtension.class)
public class VerifyInOrderMethod {
  
    @Mock
    UserService userService;
  
    @Mock
    ProductService productService;
  
    @Test
    void testInOrder() {
  
        // 이 순서로 하면 fail
        // userService.getLoginErrNum();
        // userService.getUser();

        userService.getUser();
        userService.getLoginErrNum();
  
        InOrder inOrder = inOrder(userService);
  
        inOrder.verify(userService).getUser();
        inOrder.verify(userService).getLoginErrNum();
    }
  
    @Test
    void testInOrderWithCalls() {
  
        // 이 순서로 하면 fail
        // userService.getUser();
        // userService.getLoginErrNum();
        // userService.getUser();
  
        userService.getUser();
        userService.getUser();
        userService.getLoginErrNum();
  
        InOrder inOrder = inOrder(userService);
  
        inOrder.verify(userService, calls(2)).getUser();
        inOrder.verify(userService).getLoginErrNum();
    }
  
    @Test
    void testInOrderWithVerifyNoMoreInteractions() {
        userService.getUser();
        // userService.getLoginErrNum(); - 실행하면 fail
  
        InOrder inOrder = inOrder(userService);
  
        inOrder.verify(userService).getUser();
  
        verifyNoMoreInteractions(userService); //위에 verify 이후 userService를 호출하면 fail
    }
  
    @Test
    void testInOrderWithVerifyNoInteractions() {
        userService.getUser();
        userService.getLoginErrNum();
        // productService.getProduct(); - 실행하면 fail
         
        InOrder inOrder = inOrder(userService);
  
        inOrder.verify(userService).getUser();
        inOrder.verify(userService).getLoginErrNum();
  
        verifyNoInteractions(productService); //productService를 호출하면 fail
    }
}

 

 

@Spy

  • Spy로 만든 mock 객체는 진짜 객체이며, 스터빙을 하지 않으면 기존 객체의 로직을 실행한다

Example

  • Service
public class UserService {

	public User getUser() {
        return new User("haenny", 28);	// name, age
    }

	public int getLoginErrNum() {
        return 1;	
    }

}
  • Test @Spy
@ExtendWith(MockitoExtension.class)
public class MockAnnotation {

    @Spy
    UserService userService;

    @Test
    void testSpy_스터빙X() {	// 스터빙 안한 객체는 실제 로직을 탄다
		User user = userService.getUser();
		assertEquals("haenny", user.getName());	// Test Success
    }

    @Test
    void testSpy_스터빙O() {
		User user = new User("heng", 29);
		when(userService.getUser()).thenReturn(user);	// 스터빙 했기 때문에, heng 반환
        assertEquals("haenny", userService.getUser());	// Test Fail
    }

}

 

@InjectMocks

  • @Mock이나 @Spy로 생성된 Mock 객체를 자동으로 주입해줌

Example 

  • Service
public class OrderService {
  
    UserService userService;
    ProductService productService;
  
    OrderService(UserService userService, ProductService productService) {
        this.userService = userService;
        this.productService = productService;
    }
  
    public User getUser() {
        return userService.getUser();
    }
  
    public Product getProduct() {
        return productService.getProduct();
    }
}
  • Test @InjectMocks
@ExtendWith(MockitoExtension.class)
public class InjectMocksAnnotation {
  
    @Mock
    UserService userService;
  
    @Spy
    ProductService productService;
  
    @InjectMocks
    OrderService orderService;
  
    @Test
    void testGetUser() {
        assertNull(orderService.getUser());
    }
}
  • Test @InjectMocks 사용하지 않은 경우
@ExtendWith(MockitoExtension.class)
public class InjectMocksAnnotation {
  
    @Mock
    UserService userService;
  
    @Spy
    ProductService productService;
  
    OrderService orderService;
  
    @Test
    void testGetUser() {
		orderService = new OrderService(userService, productService);
        assertNull(orderService.getUser());
    }
}
728x90
반응형

댓글

추천 글