기본적으로 단위 테스트는 하나의 가장 작은 기능을 테스트하는 것을 목표로 합니다.
하지만 하나의 애플리케이션에서 수많은 객체들은 서로 메시지를 통해 소통을 하며 의존하기 때문에 개별 기능을 단독으로 테스트하기란 쉽지 않습니다.
예를 들어 라면을 끓이기 위한 여러 단계로 물 냄비에 넣기, 물 끓이기, 스프 넣기, 면 넣기 등이 있다고 할 때, 물 끓이는 행위는 당연히 물이 냄비에 있어야 합니다.
만약 물이 없는 상태로 물을 끓이는 행위를 테스트 하면 당연히 실패할뿐더러 물을 끓이는 행위 자체에 대한 테스트를 할 수 없게 되겠지요.
그렇기에 단위 테스트를 도와줄 방법으로 Mock이란 개념이 생기게 되었습니다.
mock을 우리 말로 번역하면 '가짜의' 또는 '모의의'라는 뜻을 갖고 있습니다.
한 마디로 객체를 가짜로 만들어서 테스트를 한다는 뜻이 됩니다. 이를 통해 필요한 기능에만 단위 테스트하는 것을 집중하게 해 줍니다.
이런 객체를 Mock Object(모의 객체)라고 합니다.
이를 통해 불필요한 객체를 선언하지 않고 가짜 객체를 만들어 의존시키게 하여 실제 테스트할 객체를 만들 수 있게 합니다.
만약 테스트하려는 메서드가 가짜 객체의 응답을 필요로 한다면 Stub이라는 방법을 사용해 테스트를 합니다.
stub이란 mock 객체가 가지고 있는 일부 메서드의 기능에 집중해, 해당 메서드의 반환 값을 임의로 지정해주는 행동입니다.
요약하자면 mock 객체를 만들어주고 이 객체가 할 행동을 stub을 통해 지정해주면 복잡한 단계를 가진 로직도 최소화하여 원하는 기능만 테스트를 할 수 있고, 단위 테스트의 원칙 중 하나인 '다른 테스트에 의존하면 안 된다'를 지킬 수 있게 됩니다.
아예 객체 전체를 mocking 하는 방법 말고도 객체의 부분만 mocking 하는 것을 spy라고 합니다.
mocking 된 부분 말고는 실제 객체와 동일하게 동작합니다.

Mockito란 자바에서 mock test를 할 수 있게 만들어 주는 mock framework입니다.
오픈소스로 제작되어 이곳에서 코드를 확인할 수 있습니다.
dependencies {
testImplementation "org.mockito:mockito-core:4.5.1"
testImplementation 'org.mockito:mockito-junit-jupiter:4.5.1'
}
이 글 작성 기준 최신 버전인 4.5.1을 추가했습니다.
mockito의 기능이 들어간 부분은 mockito-core이고 JUnit과 mockito을 함께 사용하기 위해서 MockitoExtension가 들어있는 mockito-junit-jupiter를 추가합니다.
최신 버전을 보고 싶으시다면 여기를 클릭해주세요.
만약 spring boot를 사용하고 spring-boot-starter-test가 추가가 되어있다면 따로 추가하지 않으셔도 됩니다.
우선 mockito의 대표적인 기능들 몇 개를 살펴보겠습니다.
mock()/@Mock
when() 또는 given()으로 특정 행동을 지정할 수 있습니다.spy()/@Spy
@InjectMock
@Mock이 붙은 객체들을 주입시켜줍니다.public class Account {
private int money = 100;
public void deposit(int amount) {
money += amount;
}
public int withdraw(int amount) throws NoMoneyException {
if (!hasMoney() || money < amount) {
throw new NoMoneyException("돈이 없어요");
}
money -= amount;
return amount;
}
private boolean hasMoney() {
return money != 0;
}
}
public class Person {
private Account account;
public void deposit(int amount) {
account.deposit(amount);
}
public int withdraw(int amount) {
return account.withdraw(amount);
}
}
위처럼 하나의 계좌와 그 계좌를 갖고 있는 사람 객체가 있다고 가정을 해보겠습니다.
mockito를 이용하여 Account를 테스트하면 다음과 같습니다.
@ExtendWith(MockitoExtension.class)
class MockTest {
@Mock
Account account;
@Test
@DisplayName("출금 테스트")
void accountWithdrawTest() throws NoMoneyException {
//given
//account = mock(Account.class); mock 메서드를 이용하는 경우
int amount = 50;
when(account.withdraw(amount)).thenReturn(50);
//when
int result = account.withdraw(amount);
//then
assertThat(result).isEqualTo(50);
}
}
우선 JUnit과 Mockito를 함께 사용하기 위해 @ExtendWith(MockitoExtension.class)를 테스트 클래스에 붙여줍니다.
그 후 @Mock 또는 mock()을 이용해 mock 객체를 만들고 해당 객체의 행동을 stub을 통해 지정해줍니다.

그런데 BDD(Behavior-Driven Development)에 따라 코드를 작성했는데 given에 when()메서드가 있는 게 불편하게 느껴집니다.
그래서 mockito에서는 BDDMockito라는 클래스를 제공합니다.
이 객체는 Mockito를 상속한 것으로 사용법에는 크게 차이가 없습니다.
@Test
@DisplayName("출금 테스트")
void accountWithdrawTest() {
//given
int amount = 50;
given(account.withdraw(amount)).willReturn(50);
//when
int result = account.withdraw(amount);
//then
assertThat(result).isEqualTo(50);
}
위처럼 좀 더 직관적인 테스트 코드를 작성할 수 있습니다.
현재 코드는 Account때문에 Person에 대한 단위 테스트를 진행하기 힘든 상태입니다.
따라서 Person에 대한 단위 테스트를 하기 위해 mockito를 이용해보겠습니다.
@Mock
Account account;
@InjectMocks
Person person;
@Test
@DisplayName("Person 출금 테스트")
void personWithdrawTest() {
int amount = 50;
given(account.withdraw(amount)).willReturn(50);
int result = person.withdraw(amount);
assertThat(result).isEqualTo(50);
}
이런 식으로 Account의 기능이 제대로 동작한다는 가정 하에 Person에 대한 단위 테스트를 진행할 수 있습니다.

Police객체를 추가하고 Person을 다음과 같이 수정해보도록 하겠습니다.
public class Police {
public void checkSpy() throws SpyOccurredException{
throw new SpyOccurredException("스파이를 찾았다!");
}
public void catchSpy() {
throw new CaughtException("스파이를 잡았다!");
}
}
public class Person {
private Account account;
public int withdraw(int amount, Police police) {
try {
int money = account.withdraw(amount);
police.checkSpy();
return Math.max(money, Integer.MAX_VALUE);
} catch (SpyOccurredException e) {
caughtBy(police);
return Integer.MAX_VALUE;
}
}
void caughtBy(Police police) {
police.catchSpy();
}
}
스파이를 잡는 경찰과 돈을 들고 도망가려는 스파이가 있다고 가정을 해봅시다.(도둑이 조금 더 적합한 것 같지만 넘어가겠습니다^^;)
평범한 스파이라면 다음처럼 경찰에게 잡히게 됩니다.
@Mock
Account account;
@InjectMocks
Person spy;
@Test
@DisplayName("잡히는 경우")
void spyTest() {
int amount = 50;
Police police = new Police();
given(account.withdraw(amount)).willReturn(50);
assertThatThrownBy(() -> spy.withdraw(amount, police))
.isInstanceOf(CaughtException.class);
}
스파이는 잡히지 않기 위해 자신을 잡으려는 경찰이 caughtBy라는 행동을 하지 않도록 stub을 합니다.
@Mock
Account account;
@InjectMocks
Person spy;
@Test
@DisplayName("도망치는 경우")
void spyRunTest() {
int amount = 50;
Police police = new Police();
given(account.withdraw(amount)).willReturn(50);
doNothing()
.when(spy)
.caughtBy(police);
int stolenMoney = spy.withdraw(amount, police);
assertThat(stolenMoney).isEqualTo(Integer.MAX_VALUE);
}

하지만 @InjectMock이 붙어있는 객체는 mock객체가 아닌 실제 객체이므로 stub을 할 수 없습니다.
이럴 때 사용하는 것이 바로 @Spy 어노테이션입니다.
mock 객체로 선언을 하면서 원하는 기능에만 집중하여 테스트할 수 있도록 도와줍니다.
@Mock
Account account;
@Spy
@InjectMocks
Person spy;
@Test
@DisplayName("도망치는 경우")
void spyRunTest() {
int amount = 50;
Police police = new Police();
given(account.withdraw(amount)).willReturn(50);
doNothing()
.when(spy)
.caughtBy(police);
int stolenMoney = spy.withdraw(amount, police);
assertThat(stolenMoney).isEqualTo(Integer.MAX_VALUE);
}

결국 스파이는 21억 가량의 돈을 들고 달아날 수 있게 됩니다. 부럽네요
실제 개발을 할 때 static 메서드를 너무 많이 사용하면 mock을 이용한 단위 테스트를 하는데 어려움을 겪게 됩니다.
우선 왜 어려움을 겪는지 확인을 해보겠습니다.
import java.util.Random;
public class Dice {
private static Random random = new Random();
public static int role() {
return random.nextInt(6);
}
}
public class Display {
Dice dice;
public String display() {
int num = dice.role();
String str = "주사위 숫자는 " + num + " 입니다!";
System.out.println(str);
return str;
}
}
주사위와 그 주사위의 값을 보여주는 객체가 있다고 했을 때 평소처럼 mock을 이용해 Display 메서드를 테스트 해봅시다.
@ExtendWith(MockitoExtension.class)
class DisplayTest {
@Mock
Dice dice;
@InjectMocks
Display display;
@Test
void displayTest() {
given(dice.role()).willReturn(6);
String displayedStr = this.display.display();
assertThat(displayedStr).isEqualTo("주사위 숫자는 6 입니다!");
}
}

결과는 우리가 예상한 대로가 아닌 MissingMethodInvocationException이 발생합니다.
생각해보면 당연한 결과입니다.
아무리 우리가 static메서드를 Object.method()형식으로 적었다고 하지만 .class파일에 가보면 Class.method()의 형식으로 바뀌어 있습니다.
이렇게 되면 Object 레벨의 테스트가 아닌 Class 레벨의 테스트가 되고 더 이상 객체에 대한 단위 테스트라고 할 수가 없게 됩니다.
또한 스택오버플로우에 있는 답변을 보면 Mock 객체는 런타임 도중에 해당 객체를 인터페이스화 하거나 상속을 하기 때문에 상속이 되지 않는 static 메서드에 대해서는 처리를 하지 않는다는 추측이 있었습니다.
메서드 상속이 되지 않는 private, 오버라이드 할 수 없는 fianl에 대해서는 에러를 발생시킨다는 문구가 위의 에러에 있는 것만 봐도 상당히 신빙성 있는 추측이라고 생각됩니다.
물론 mockStatic()메서드, PowerMock 프레임워크 등을 이용하여 처리를 할 수 있지만 애초에 이렇게 테스트를 해야 되는 코드 자체가 객체지향적으로 설계된 코드가 아닐 확률이 높기 때문에 가능하면 지양하는 편이 좋다고 합니다.
Mock에 관한 자료를 찾아보다가 관련 용어들을 공부하는데 'Mock', 'Spy', 'Stub'의 정의가 개인적으로 아무리 봐도 잘 이해가 안 되었습니다. 그래서 Mockito의 기능 위주로 설명을 적었는데 언젠가 제대로 이해하는 날이 오면 추가로 작성하도록 하겠습니다..😂
https://jojoldu.tistory.com/239
https://stackoverflow.com/questions/4482315/why-doesnt-mockito-mock-static-methods
도움 많이 되었습니다.