A Unit Testing Practitioner's Guide to Everyday Mockito
게시 됨: 2022-03-11단위 테스트는 애자일 시대에 필수가 되었으며 자동화된 테스트를 지원하는 데 사용할 수 있는 도구가 많이 있습니다. 그러한 도구 중 하나는 테스트를 위해 모의 개체를 만들고 구성할 수 있는 오픈 소스 프레임워크인 Mockito입니다.
이 기사에서는 모의 객체를 생성 및 구성하고 이를 사용하여 테스트 중인 시스템의 예상 동작을 확인하는 방법에 대해 설명합니다. 또한 Mockito의 디자인과 주의 사항을 더 잘 이해하기 위해 Mockito의 내부에 대해 자세히 알아보겠습니다. 우리는 JUnit을 단위 테스트 프레임워크로 사용할 것이지만 Mockito는 JUnit에 바인딩되지 않으므로 다른 프레임워크를 사용하더라도 따라할 수 있습니다.
Mockito 얻기
요즘 Mockito를 얻는 것은 쉽습니다. Gradle을 사용하는 경우 빌드 스크립트에 다음 한 줄을 추가하면 됩니다.
testCompile "org.mockito:mockito−core:2.7.7"
여전히 Maven을 선호하는 나와 같은 사람들은 다음과 같이 종속성에 Mockito를 추가하기만 하면 됩니다.
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
물론 세상은 Maven과 Gradle보다 훨씬 넓습니다. Maven 중앙 저장소에서 Mockito jar 아티팩트를 가져오기 위해 모든 프로젝트 관리 도구를 자유롭게 사용할 수 있습니다.
접근하는 목토
단위 테스트는 종속성의 동작에 의존하지 않고 특정 클래스 또는 메서드의 동작을 테스트하도록 설계되었습니다. 우리는 코드의 가장 작은 '단위'를 테스트하고 있기 때문에 이러한 종속성의 실제 구현을 사용할 필요가 없습니다. 또한 다른 동작을 테스트할 때 이러한 종속성의 약간 다른 구현을 사용합니다. 이에 대한 기존의 잘 알려진 접근 방식은 '스텁'(특정 시나리오에 적합한 인터페이스의 특정 구현)을 만드는 것입니다. 이러한 구현에는 일반적으로 하드 코딩된 논리가 있습니다. 스텁은 일종의 테스트 더블입니다. 다른 종류에는 가짜, 모의, 스파이, 인형 등이 포함됩니다.
우리는 Mockito에서 많이 사용하는 'mocks'와 'spies'의 두 가지 유형의 테스트 더블에만 초점을 맞출 것입니다.
모의
조롱이란 무엇입니까? 분명히, 그것은 동료 개발자를 놀리는 곳이 아닙니다. 단위 테스트를 위한 조롱은 제어된 방식으로 실제 하위 시스템의 동작을 구현하는 개체를 만들 때입니다. 간단히 말해서, 모의 객체는 종속성을 대체하는 데 사용됩니다.
Mockito를 사용하면 모의 객체를 만들고 특정 메서드가 호출될 때 수행할 작업을 Mockito에 말한 다음 실제 대신 테스트에서 모의 인스턴스를 사용합니다. 테스트 후, 어떤 특정 메소드가 호출되었는지 확인하기 위해 모의 쿼리를 수행하거나 변경된 상태의 형태로 부작용을 확인할 수 있습니다.
기본적으로 Mockito는 모의의 모든 메서드에 대한 구현을 제공합니다.
스파이
스파이는 Mockito가 생성하는 다른 유형의 테스트 더블입니다. 모의 객체와 달리 스파이를 생성하려면 감시할 인스턴스가 필요합니다. 기본적으로 스파이는 모든 메서드 호출을 실제 개체에 위임하고 호출된 메서드와 매개 변수를 기록합니다. 그것이 스파이가 되는 이유입니다. 실제 물체를 염탐하는 것입니다.
가능하면 스파이 대신 모의 사용을 고려하십시오. 스파이는 쉽게 테스트할 수 있도록 다시 설계할 수 없는 레거시 코드를 테스트하는 데 유용할 수 있지만 스파이를 사용하여 클래스를 부분적으로 조롱해야 하는 것은 클래스가 너무 많은 일을 하고 있어 단일 책임 원칙을 위반한다는 표시입니다.
간단한 예제 만들기
테스트를 작성할 수 있는 간단한 데모를 살펴보겠습니다. 식별자로 사용자를 찾는 단일 메서드가 있는 UserRepository
인터페이스가 있다고 가정합니다. 또한 일반 텍스트 암호를 암호 해시로 바꾸는 암호 인코더 개념이 있습니다. UserRepository
와 PasswordEncoder
는 모두 생성자를 통해 주입된 UserService
의 종속성(협력자라고도 함)입니다. 데모 코드는 다음과 같습니다.
사용자 저장소
public interface UserRepository { User findById(String id); }
사용자
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
암호 인코더
public interface PasswordEncoder { String encode(String password); }
사용자 서비스
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
이 예제 코드는 GitHub에서 찾을 수 있으므로 이 문서와 함께 검토를 위해 다운로드할 수 있습니다.
Mockito 적용하기
예제 코드를 사용하여 Mockito를 적용하고 몇 가지 테스트를 작성하는 방법을 살펴보겠습니다.
모의 만들기
Mockito를 사용하면 Mockito.mock()
정적 메서드를 호출하는 것만큼 쉽게 모의 객체를 생성할 수 있습니다.
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Mockito의 정적 가져오기를 확인하세요. 이 기사의 나머지 부분에서는 이 가져오기가 추가된 것으로 암시적으로 고려할 것입니다.
가져오기 후에 인터페이스인 PasswordEncoder
를 조롱합니다. Mockito는 인터페이스뿐만 아니라 추상 클래스와 구체적인 비 최종 클래스도 모의합니다. 기본적으로 Mockito는 최종 클래스와 최종 또는 정적 메서드를 모의할 수 없지만 실제로 필요한 경우 Mockito 2는 실험적인 MockMaker 플러그인을 제공합니다.
또한 equals()
및 hashCode()
메서드는 조롱할 수 없습니다.
스파이 만들기
스파이를 생성하려면 Mockito의 정적 메서드 spy()
를 호출하고 스파이할 인스턴스를 전달해야 합니다. 반환된 객체의 호출 메서드는 해당 메서드가 스텁되지 않는 한 실제 메서드를 호출합니다. 이러한 호출은 기록되고 이러한 호출의 사실을 확인할 수 있습니다( verify()
에 대한 추가 설명 참조). 스파이를 만들어 봅시다.
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
스파이를 생성하는 것은 모의 객체를 생성하는 것과 크게 다르지 않습니다. 또한 모의 구성에 사용되는 모든 Mockito 방법은 스파이 구성에도 적용할 수 있습니다.
스파이는 모의 객체에 비해 거의 사용되지 않지만, 리팩토링할 수 없는 레거시 코드를 테스트하는 데 유용할 수 있습니다. 이러한 경우, 원하는 동작을 얻기 위해 스파이를 만들고 그 방법 중 일부를 스텁할 수 있습니다.
기본 반환 값
mock(PasswordEncoder.class)
를 호출하면 PasswordEncoder
의 인스턴스가 반환됩니다. 메서드를 호출할 수도 있지만 반환되는 내용은 무엇입니까? 기본적으로 모의의 모든 메서드는 "초기화되지 않은" 또는 "빈" 값을 반환합니다. 예를 들어 숫자 유형(기본 및 박스형 모두)의 경우 0, 부울의 경우 false, 대부분의 다른 유형의 경우 null입니다.
다음 인터페이스를 고려하십시오.
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
이제 모의 메소드에서 기대할 수 있는 기본값에 대한 아이디어를 제공하는 다음 스니펫을 고려하십시오.
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
스터빙 방법
신선하고 변경되지 않은 모의 객체는 드문 경우에만 유용합니다. 일반적으로 우리는 모의를 구성하고 모의의 특정 메서드가 호출될 때 수행할 작업을 정의하려고 합니다. 이것을 스터빙 이라고 합니다.
Mockito는 두 가지 스텁 방법을 제공합니다. 첫 번째 방법은 "이 메서드가 호출 되면 조치 를 취하는 것"입니다. 다음 스니펫을 고려하십시오.
when(passwordEncoder.encode("1")).thenReturn("a");
영어와 거의 비슷합니다. " passwordEncoder.encode(“1”)
이 호출되면 a
를 반환합니다."
스터빙의 두 번째 방법은 "이 목의 메서드가 다음 인수와 함께 호출될 때 작업을 수행합니다."와 같이 읽습니다. 이 스텁 방식은 끝에 원인이 지정되어 있으므로 읽기가 더 어렵습니다. 고려하다:
doReturn("a").when(passwordEncoder).encode("1");
이 스터빙 메서드가 있는 스니펫은 " passwordEncoder
의 encode()
메서드가 인수 1
로 호출될 때 반환 a
."
첫 번째 방법은 형식이 안전하고 가독성이 높기 때문에 선호되는 것으로 간주됩니다. 그러나 드물게, 스파이의 실제 메소드를 스터빙할 때와 같이 두 번째 방법을 강제로 사용해야 하는 경우가 있습니다. 호출하면 원치 않는 부작용이 있을 수 있기 때문입니다.
Mockito가 제공하는 stubbing 메소드를 간단히 살펴보자. 예제에 스터빙의 두 가지 방법을 모두 포함할 것입니다.
반환 값
thenReturn
또는 doReturn()
은 메서드 호출 시 반환될 값을 지정하는 데 사용됩니다.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
또는
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
연속 메서드 호출의 결과로 반환될 여러 값을 지정할 수도 있습니다. 마지막 값은 이후의 모든 메서드 호출에 대한 결과로 사용됩니다.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
또는
//do doReturn("a", "b").when(passwordEncoder).encode("1");
다음 스니펫을 사용하여 동일한 결과를 얻을 수 있습니다.
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
이 패턴은 다른 스터빙 방법과 함께 사용하여 연속 호출의 결과를 정의할 수도 있습니다.
사용자 정의 응답 반환
then()
, thenAnswer()
의 별칭 및 doAnswer()
는 동일한 결과를 얻었습니다. 이는 다음과 같이 메서드가 호출될 때 반환될 사용자 지정 답변을 설정하는 것입니다.
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
또는
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
thenAnswer()
가 취하는 유일한 인수는 Answer
인터페이스의 구현입니다. InvocationOnMock
유형의 매개변수가 있는 단일 메소드가 있습니다.
메서드 호출의 결과로 예외를 throw할 수도 있습니다.
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
...또는 클래스의 실제 메서드를 호출합니다(인터페이스에는 적용되지 않음):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
번거롭다고 생각하시면 맞습니다. Mockito는 테스트의 이러한 측면을 간소화하기 위해 thenCallRealMethod thenCallRealMethod()
및 thenThrow()
를 제공합니다.
실제 메서드 호출
이름에서 알 수 있듯이 thenCallRealMethod()
및 doCallRealMethod()
는 모의 객체에서 실제 메서드를 호출합니다.
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
실제 메서드를 호출하는 것은 부분적인 모의에 유용할 수 있지만 호출된 메서드에 원치 않는 부작용이 없고 객체 상태에 의존하지 않는지 확인하십시오. 그렇다면 스파이가 모의 것보다 더 적합할 수 있습니다.
인터페이스의 모형을 만들고 실제 메서드를 호출하도록 스텁을 구성하려고 하면 Mockito는 매우 유익한 메시지와 함께 예외를 throw합니다. 다음 스니펫을 고려하십시오.
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito는 다음 메시지와 함께 실패합니다.
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
이러한 철저한 설명을 제공할 만큼 충분히 신경써주신 Mockito 개발자들에게 감사드립니다!
예외 던지기
thenThrow()
및 doThrow()
는 예외를 throw하도록 모의 메서드를 구성합니다.
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
또는
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito는 throw되는 예외가 해당 특정 스텁 메서드에 대해 유효한지 확인하고 메서드의 확인된 예외 목록에 예외가 없으면 불평합니다. 다음을 고려하세요:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
오류가 발생합니다.
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
보시다시피 Mockito는 encode()
가 IOException
을 던질 수 없다는 것을 감지했습니다.
예외의 인스턴스를 전달하는 대신 예외의 클래스를 전달할 수도 있습니다.
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
또는
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
즉, Mockito는 예외 인스턴스의 유효성을 검사하는 것과 같은 방식으로 예외 클래스의 유효성을 검사할 수 없으므로 훈련을 받고 잘못된 클래스 개체를 전달하지 않아야 합니다. 예를 들어, 다음은 encode()
가 확인된 예외를 throw할 것으로 예상되지 않지만 IOException
을 throw합니다.
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
기본 메서드를 사용하여 인터페이스를 조롱하기
인터페이스에 대한 모의 객체를 생성할 때 Mockito는 해당 인터페이스의 모든 메소드를 모의한다는 점은 주목할 가치가 있습니다. Java 8부터 인터페이스에는 추상 메서드와 함께 기본 메서드가 포함될 수 있습니다. 이러한 메서드도 조롱되므로 기본 메서드로 작동하도록 주의해야 합니다.
다음 예를 고려하십시오.
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
이 예에서는 assertFalse()
가 성공합니다. 예상한 것이 아니라면 Mockito가 다음과 같이 실제 메서드를 호출하도록 했는지 확인하세요.
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
인수 일치자
이전 섹션에서 정확한 값을 인수로 사용하여 모의 메서드를 구성했습니다. 이러한 경우 Mockito는 내부적으로 equal equals()
을 호출하여 예상 값이 실제 값과 같은지 확인합니다.
그러나 때때로 우리는 이러한 값을 미리 알지 못합니다.
아마도 우리는 인수로 전달되는 실제 값에 대해 신경 쓰지 않거나 더 넓은 범위의 값에 대한 반응을 정의하기를 원할 수 있습니다. 이러한 모든 시나리오(및 그 이상)는 인수 일치기로 해결할 수 있습니다. 아이디어는 간단합니다. 정확한 값을 제공하는 대신 Mockito가 메서드 인수와 일치시킬 인수 일치자를 제공합니다.
다음 스니펫을 고려하십시오.
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
첫 번째 줄에서 anyString()
인수 매처를 사용했기 때문에 우리가 encode()
에 어떤 값을 전달하든 결과가 동일하다는 것을 알 수 있습니다. 해당 줄을 일반 영어로 다시 작성하면 "암호 인코더가 문자열을 인코딩하도록 요청받은 경우 '정확한' 문자열을 반환합니다."와 같이 들릴 것입니다.
Mockito는 매처 또는 정확한 값으로 모든 인수를 제공해야 합니다. 따라서 메서드에 둘 이상의 인수가 있고 일부 인수에 대해서만 인수 일치자를 사용하려는 경우 해당 인수를 잊어버리십시오. 다음과 같은 코드는 작성할 수 없습니다.
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
오류를 수정하려면 다음과 같이 에 a
eq
인수 일치자를 포함하도록 마지막 줄을 바꿔야 합니다.
when(mock.call(eq("a"), anyInt())).thenReturn(true);
여기에서는 eq()
및 anyInt()
인수 일치자를 사용했지만 다른 많은 항목도 사용할 수 있습니다. 인수 일치자의 전체 목록은 org.mockito.ArgumentMatchers
클래스에 대한 문서를 참조하십시오.
확인 또는 스터빙 외부에서 인수 일치자를 사용할 수 없다는 점에 유의하는 것이 중요합니다. 예를 들어 다음을 가질 수 없습니다.
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito는 잘못 배치된 인수 matcher를 감지하고 InvalidUseOfMatchersException
을 발생시킵니다. 인수 일치자를 사용한 검증은 다음과 같이 수행되어야 합니다.
verify(mock).encode(or(eq("a"), endsWith("b")));
인수 매처도 반환 값으로 사용할 수 없습니다. Mockito는 anyString()
또는 무엇이든 반환할 수 없습니다. 호출을 스텁할 때 정확한 값이 필요합니다.
사용자 지정 일치자
사용자 지정 매처는 Mockito에서 아직 사용할 수 없는 일부 일치 논리를 제공해야 할 때 구출됩니다. 사소하지 않은 방식으로 인수를 일치시켜야 하는 필요성은 설계에 문제가 있거나 테스트가 너무 복잡해지고 있음을 나타내기 때문에 사용자 정의 매처를 생성하기로 한 결정을 가볍게 여겨서는 안 됩니다.
따라서 사용자 지정 매처를 작성하기 전에 isNull()
및 nullable()
)과 같은 관대한 인수 매처를 사용하여 테스트를 단순화할 수 있는지 여부를 확인하는 것이 좋습니다. 여전히 인수 매처를 작성해야 할 필요성을 느낀다면 Mockito는 이를 수행하는 메소드 제품군을 제공합니다.
다음 예를 고려하십시오.
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
여기서 우리는 hasLuck
인수 매처를 만들고 argThat()
을 사용하여 매처를 조롱된 메서드에 인수로 전달하고 파일 이름이 "luck"으로 끝나는 경우 true
를 반환하도록 스텁합니다. ArgumentMatcher
를 기능적 인터페이스로 취급하고 람다를 사용하여 해당 인스턴스를 생성할 수 있습니다(예제에서 수행한 작업). 덜 간결한 구문은 다음과 같습니다.
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
프리미티브 유형과 작동하는 인수 매처를 생성해야 하는 경우 org.mockito.ArgumentMatchers
에 이에 대한 몇 가지 다른 메소드가 있습니다.
- charThat(ArgumentMatcher<Character>매처)
- booleanThat(ArgumentMatcher<Boolean> 매처)
- byteThat(ArgumentMatcher<바이트> 매처)
- shortThat(ArgumentMatcher<Short> 매처)
- intThat(ArgumentMatcher<정수>매처)
- longThat(ArgumentMatcher<Long> 매처)
- floatThat(ArgumentMatcher<Float>매처)
- doubleThat(ArgumentMatcher<Double> 매처)
매처 결합
조건이 너무 복잡하여 기본 매처로 처리할 수 없는 경우 사용자 지정 인수 매처를 만드는 것이 항상 가치가 있는 것은 아닙니다. 때때로 matchers를 결합하는 것이 트릭을 할 것입니다. Mockito는 기본 유형과 비기본 유형 모두와 일치하는 인수 일치자에서 공통 논리 연산('not', 'and', 'or')을 구현하기 위해 인수 일치자를 제공합니다. 이러한 매처는 org.mockito.AdditionalMatchers
클래스에서 정적 메서드로 구현됩니다.
다음 예를 고려하십시오.
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
여기에서는 eq("1")
및 contains("a")
의 두 인수 일치자의 결과를 결합했습니다. 최종 표현식 or(eq("1"), contains("a"))
는 "인수 문자열은 "1" 과 같아야 하거나 "a"를 포함 해야 한다고 해석될 수 있습니다.
org.mockito.AdditionalMatchers
클래스에 나열되는 덜 일반적인 일치자가 있다는 점에 유의하십시오. 예를 들어 geq()
, leq()
, gt()
및 lt()
는 원시 값과 java.lang.Comparable
.
행동 확인
모의 또는 스파이가 사용되면 특정 상호 작용이 발생 verify
수 있습니다. 문자 그대로, 우리는 "Hey, Mockito, 이 메서드가 이러한 인수로 호출되었는지 확인하십시오."라고 말하고 있습니다.
다음과 같은 인위적인 예를 고려하십시오.
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
여기에서 우리는 mock을 설정하고 그것의 encode()
메소드를 호출했습니다. 마지막 줄은 모의의 encode()
메서드가 특정 인수 값 a
로 호출되었는지 확인합니다. 스텁된 호출을 확인하는 것은 중복됩니다. 이전 스니펫의 목적은 일부 상호작용이 발생한 후 검증을 수행한다는 아이디어를 보여주는 것입니다.
다른 인수(예: b
)를 갖도록 마지막 행을 변경하면 이전 테스트가 실패하고 Mockito는 실제 호출에 다른 인수(예상 a
대신 b
)가 있다고 불평할 것입니다.
인수 매처는 스터빙과 마찬가지로 검증에 사용할 수 있습니다.
verify(passwordEncoder).encode(anyString());
기본적으로 Mockito는 메서드가 한 번 호출되었는지 확인하지만 원하는 수의 호출을 확인할 수 있습니다.
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
거의 사용되지 않는 verify()
기능은 시간 초과 시 실패하는 기능으로, 이는 주로 동시 코드를 테스트하는 데 유용합니다. 예를 들어, 암호 인코더가 verify()
와 동시에 다른 스레드에서 호출되는 경우 다음과 같이 테스트를 작성할 수 있습니다.

usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
이 테스트는 encode()
가 호출되고 500밀리초 이하 내에 완료되면 성공합니다. 지정한 전체 기간을 기다려야 하는 경우 timeout()
after()
를 사용하십시오.
verify(passwordEncoder, after(500)).encode("a");
다른 검증 모드( times()
, atLeast()
등)를 timeout()
및 after()
)와 결합하여 더 복잡한 테스트를 수행할 수 있습니다.
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
times()
외에도 지원되는 확인 모드에는 only()
, atLeast()
및 atLeastOnce()
( atLeast(1)
의 별칭으로 사용)가 포함됩니다.
Mockito를 사용하면 모의 그룹에서 호출 순서를 확인할 수도 있습니다. 자주 사용하는 기능은 아니지만 호출 순서가 중요한 경우 유용할 수 있습니다. 다음 예를 고려하십시오.
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
시뮬레이션된 호출의 순서를 재정렬하면 VerificationInOrderFailure
와 함께 테스트가 실패합니다.
호출의 부재는 또한 verifyZeroInteractions()
를 사용하여 확인할 수 있습니다. 이 메소드는 모의 또는 모의를 인수로 받아들이고 모의에 전달된 메소드가 호출되면 실패합니다.
또한 verifyNoMoreInteractions()
메서드를 언급할 가치가 있습니다. 모의를 인수로 사용하고 해당 모의에 대한 모든 호출이 확인되었는지 확인하는 데 사용할 수 있기 때문입니다.
인수 캡처
특정 인수로 메서드가 호출되었는지 확인하는 것 외에도 Mockito를 사용하면 해당 인수를 캡처하여 나중에 사용자 지정 주장을 실행할 수 있습니다. 즉, "Hey, Mockito, 이 메서드가 호출되었는지 확인하고 호출된 인수 값을 알려주세요."라고 말하는 것입니다.
PasswordEncoder
모의를 만들고, encode()
를 호출하고, 인수를 캡처하고, 값을 확인합시다.
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
보시다시피 확인을 위해 passwordCaptor.capture()
를 encode()
의 인수로 전달합니다. 이것은 내부적으로 인수를 저장하는 인수 매처를 생성합니다. 그런 다음 passwordCaptor.getValue()
로 캡처된 값을 검색하고 assertEquals()
로 검사합니다.
여러 호출에서 인수를 캡처해야 하는 경우 ArgumentCaptor
를 사용하면 다음과 같이 getAllValues()
를 사용하여 모든 값을 검색할 수 있습니다.
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
가변 arity 메서드 인수(varargs라고도 함)를 캡처하는 데 동일한 기술을 사용할 수 있습니다.
간단한 예제 테스트하기
이제 Mockito에 대해 더 많이 알게 되었으므로 데모로 돌아갈 시간입니다. isValidUser
메서드 테스트를 작성해 보겠습니다. 다음과 같이 보일 수 있습니다.
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
API 아래에서 다이빙
Mockito는 읽기 쉽고 편리한 API를 제공하지만 제한 사항을 이해하고 이상한 오류를 피하기 위해 내부 작동 중 일부를 살펴보겠습니다.
다음 스니펫이 실행될 때 Mockito 내부에서 무슨 일이 일어나고 있는지 살펴보겠습니다.
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
분명히 첫 번째 줄은 모의 객체를 만듭니다. Mockito는 ByteBuddy를 사용하여 주어진 클래스의 하위 클래스를 생성합니다. 새 클래스 객체는 demo.mockito.PasswordEncoder$MockitoMock$1953422997
과 같은 생성된 이름을 가지며 equals()
는 신원 확인 역할을 하고 hashCode()
는 신원 해시 코드를 반환합니다. 클래스가 생성되고 로드되면 Objenesis를 사용하여 해당 인스턴스가 생성됩니다.
다음 줄을 살펴보겠습니다.
when(mock.encode("a")).thenReturn("1");
순서가 중요합니다. 여기서 실행된 첫 번째 명령문은 mock.encode("a")
이며 기본 반환 값이 null
인 모의에 대해 encode()
를 호출합니다. 따라서 실제로 when()
의 인수로 null
을 전달합니다. Mockito는 해당 메서드가 호출될 때 소위 '진행 중인 스터빙'에 조롱된 메서드의 호출에 대한 정보를 저장하기 때문에 when()
에 전달되는 정확한 값을 신경 쓰지 않습니다. 나중에 when()
을 호출할 때 Mockito는 진행중인 스터빙 객체를 가져와 when when()
) 의 결과로 반환합니다. 그런 다음 반환된 진행 중인 스터빙 개체에 대해 thenReturn(“1”)
을 호출합니다.
세 번째 줄, mock.encode("a");
간단합니다. 우리는 stubbed 메서드를 호출합니다. 내부적으로 Mockito는 추가 확인을 위해 이 호출을 저장하고 스텁된 호출 응답을 반환합니다. 우리의 경우 문자열 1
입니다.
네 번째 줄( verify(mock).encode(or(eq("a"), endsWith("b")));
)에서, 우리는 Mockito에게 그것들과 함께 encode()
가 호출되었는지 확인하도록 요청하고 있습니다. 특정 주장.
verify()
가 먼저 실행되어 Mockito의 내부 상태를 확인 모드로 전환합니다. Mockito가 ThreadLocal
에 상태를 유지한다는 것을 이해하는 것이 중요합니다. 이렇게 하면 멋진 구문을 구현할 수 있지만 프레임워크가 부적절하게 사용되는 경우 이상한 동작이 발생할 수 있습니다(예: 확인 또는 스터빙 외부에서 인수 일치자를 사용하려고 시도하는 경우).
그렇다면 Mockito는 or
matcher를 어떻게 생성합니까? 먼저 eq("a")
가 호출되고 equals
matcher가 matchers 스택에 추가됩니다. 두 번째로, endsWith("b")
가 호출되고 endsWith
처가 스택에 추가됩니다. 마침내 or(null, null)
이 호출됩니다. 스택에서 팝한 두 개의 매처를 사용하고 or
매처를 만들고 스택에 푸시합니다. 마지막으로, encode()
가 호출됩니다. 그런 다음 Mockito는 메서드가 예상된 횟수와 예상 인수로 호출되었는지 확인합니다.
인수 일치자는 변수로 추출할 수 없지만(호출 순서를 변경하기 때문에) 메서드로 추출할 수 있습니다. 이렇게 하면 호출 순서가 유지되고 스택이 올바른 상태로 유지됩니다.
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
기본 답변 변경
이전 섹션에서 우리는 모의 메소드가 호출될 때 "빈" 값을 반환하는 방식으로 모의를 만들었습니다. 이 동작은 구성할 수 있습니다. Mockito에서 제공하는 것이 적합하지 않은 경우 org.mockito.stubbing.Answer
자체 구현을 제공할 수도 있지만 단위 테스트가 너무 복잡해지면 무언가 잘못되었다는 표시일 수 있습니다. KISS 원칙을 기억하십시오!
미리 정의된 기본 답변을 제공하는 Mockito를 살펴보겠습니다.
RETURNS_DEFAULTS
는 기본 전략입니다. 모의를 설정할 때 명시적으로 언급할 가치가 없습니다.CALLS_REAL_METHODS
는 스텁되지 않은 호출이 실제 메서드를 호출하도록 합니다.RETURNS_SMART_NULLS
는 스텁되지 않은 메서드 호출에서 반환된 개체를 사용할 때null
대신SmartNull
을 반환하여NullPointerException
을 방지합니다. 여전히NullPointerException
으로 실패하지만SmartNull
은 unstubbed 메서드가 호출된 라인과 함께 더 나은 스택 추적을 제공합니다. 이것은 RETURNS_SMART_NULLS를RETURNS_SMART_NULLS
의 기본 답변으로 하는 것이 좋습니다!RETURNS_MOCKS
는 먼저 일반 "빈" 값을 반환하려고 시도한 다음 가능한 경우 모의하고 그렇지 않은 경우null
을 반환합니다. 비어 있음의 기준은 앞에서 본 것과 약간 다릅니다. 문자열과 배열에 대해null
을 반환하는 대신RETURNS_MOCKS
로 생성된 모의 객체는 각각 빈 문자열과 빈 배열을 반환합니다.RETURNS_SELF
는 빌더를 조롱하는 데 유용합니다. 이 설정을 사용하면 모의 클래스의 클래스(또는 수퍼 클래스)와 같은 유형을 반환하는 메서드가 호출되면 모의가 자체 인스턴스를 반환합니다.RETURNS_DEEP_STUBS
는RETURNS_MOCKS
보다 더 깊어지고 mock의 mock에서 mock을null
할 수 있는RETURNS_MOCKS
을RETURNS_DEEP_STUBS
합니다.
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
모의 이름 지정
Mockito를 사용하면 모의 이름을 지정할 수 있습니다. 테스트에 모의가 많고 구별해야 하는 경우에 유용한 기능입니다. 즉, 모의 이름을 지정해야 하는 것은 잘못된 디자인의 증상일 수 있습니다. 다음을 고려하세요:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito는 불평할 것이지만, 우리가 공식적으로 mock의 이름을 지정하지 않았기 때문에 우리는 어떤 것을 모릅니다:
Wanted but not invoked: passwordEncoder.encode(<any string>);
생성 시 문자열을 전달하여 이름을 지정해 보겠습니다.
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
이제 오류 메시지가 더 친숙하고 명확하게 robustPasswordEncoder
를 가리킵니다.
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
다중 모의 인터페이스 구현
때로는 여러 인터페이스를 구현하는 모의 객체를 만들고 싶을 수도 있습니다. Mockito는 다음과 같이 쉽게 할 수 있습니다.
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. 다음 예를 고려하십시오.
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
다른 설정
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.