1. 테스트 코드를 왜 작성해야 할까?
1-1. 유닛 테스트
- 정의 : 모든 함수와 메소드에 대한 테스트 케이스를 작성하는 절차를 말한다. 이를 통해서 언제라도 코드 변경으로 인해 문제가 발생할 경우, 단시간 내에 이를 파악하고 바로 잡을 수 있도록 해준다.
1-2. 유닛 테스트의 장점
문제점 발견
유닛 테스트의 목적은 프로그램의 각 부분을 고립 시켜서 각각의 부분이 정확하게 동작하는지 확인하는 것이다. 즉, 프로그램을 작은 단위로 쪼개서 각 단위가 정확하게 동작하는지 검사하고 이를 통해 문제 발생 시 정확하게 어느 부분이 잘못되었는지를 재빨리 확인할 수 있게 해준다. 따라서 프로그램의 안정성이 높아진다. 유닛 테스트는 일견 개발 시간을 증가 시키는 것처럼 보이지만 개발 기간 중 대부분을 차지하는 디버깅 시간을 단축시킴으로써 여유로운 프로그래밍을 가능케 한다.
변경이 쉽다
프로그래머는 언제라도 유닛 테스트를 믿고 리팩토링을 할 수 있다. 리팩토링 후에도 해당 모듈이 의도대로 작동하고 있음을 유닛 테스트를 통해서 확신할 수 있다. 이를 회귀 테스트(Regression testing)라 한다. 어떻게 코드를 고치더라도 문제점을 금방 파악할 수 있고 수정된 코드가 정확하게 동작하는지 쉽게 알 수 있게 되므로 프로그래머들은 더욱 더 의욕적으로 코드를 변경할 수 있게 된다. 좋은 유닛 테스트 디자인은 그 유닛이 사용되는 모든 경로를 커버할 수 있는 테스트 케이스를 만들어 준다.
지속적인 유닛 테스트 환경을 구축하면 어떠한 변화가 있더라도 코드와 그 실행이 의도대로 인지를 확인하고 검증 할 수 있게 된다. 확립된 개발 방법과 유닛 테스트의 범위에 따라서 프로그램의 정확성이 좌우된다.
통합이 간단하다
유닛 테스트는 유닛 자체의 불확실성을 제거해주므로 상향식(bottom-up) 테스트 방식에서 유용하다. 먼저 프로그램의 각 부분을 검증하고 그 부분들은 합쳐서 다시 검증하는 통합 테스트에서 더욱 더 빛을 발한다.
2. Junit5
- junit은 자바 기반 코드를 테스트하기 위한 유닛 테스트 프레임워크이다.
- junit5는 자바 8 버전 이상에서 동작하는 최신 버전이고, 세 개의 서브 프로젝트로 이루어져 있다.
- junit platform : jvm에서 테스트 프레임워크를 실행하는데 필요한 기초를 제공
- junit jupiter : junit에서 테스트를 구성하기 위한 어노테이션들과 메소드들을 제공
- junit vintage : 레거시 테스트 마이그레이션을 지원하는 패키지
- gradle로 시작하기
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}
test {
useJUnitPlatform()
}
2-1. junit 사용법
lifecycle method
- 테스트 메소드 생명주기
@BeforeAll
- 테스트 클래스를 초기화할 때 한번만 수행되는 메소드
- static으로 선언
- 테스트 클래스 안에 여러 테스트 메소드들이 있어도 최초 한번만 실행
@BeforeAll
static void setup() {
System.out.println("@BeforeAll");
testStr = "test";
}
@Test
public void testA() {
testStr += "1";
assertEquals("test1", testStr);
}
@Test
public void testB() {
testStr += "1";
assertEquals("test11", testStr);
}
// setup >> testA >> testB 순서로 실행
- 테스트 메소드 실행 순서는 기본적으로 이름순이며, @Order 어노테이션으로 테스트 실행 순서를 정해줄 수도 있음.
@BeforeEach
- 각 테스트 메소드 실행 전 메소드를 실행
@BeforeEach
public void setup() {
System.out.println("@BeforeEach");
testStr = "test";
}
@Test
public void testA() {
testStr += "1";
assertEquals("test1", testStr);
}
@Test
public void testB() {
testStr += "1";
assertEquals("test1", testStr);
}
// setup >> testA >> setup >> testB 순서로 실행
@AfterAll
- 모든 테스트 메소드를 실행시킨 후 딱 한번만 수행
- static으로 선언
@Test
public void testA() {
testStr += "1";
assertEquals("test1", testStr);
}
@Test
public void testB() {
testStr += "1";
assertEquals("test1", testStr);
}
@AfterAll
public void afterAll() {
System.out.println("@AfterAll");
file.close();
}
// testA >> testB >> afterAll 순서로 실행
@AfterEach
- 테스트 메소드 실행 이후에 수행
@Test
public void testA() {
testStr += "1";
assertEquals("test1", testStr);
}
@Test
public void testB() {
testStr += "1";
assertEquals("test1", testStr);
}
@AfterEach
public void afterAll() {
System.out.println("@AfterEach");
}
// testA >> afterEach >> testB >> afterEach 순서로 실행
2-2. assertion
assertEquals
- assertEquals(Object expected, Object actual)
- 기대값과 실제값을 비교해 동일하면 테스트를 통과한다.
@AllArgsConstructor
@EqualsAndHashCode
static class Parameter {
private String keyword;
}
@Test
public void equalsTest() {
Parameter parameter1 = new Parameter("우유");
Parameter parameter2 = new Parameter("우유");
assertEquals(parameter1, parameter2);
}
- 실패 메세지 미정의시
- 실패시 출력할 메세지를 추가할수도 있다.
@AllArgsConstructor
static class Parameter extends Object {
private String keyword;
}
@Test
public void equalsTest() {
String failMessage = "객체의 equals와 hashCode를 재정의해야 합니다.";
Parameter parameter1 = new Parameter("우유");
Parameter parameter2 = new Parameter("우유");
assertEquals(parameter1, parameter2, failMessage);
}
assertNotEquals
- assertNotEquals(Object unexpected, Object actual)
- 기대하지 않은 값을 실제값과 비교해 동일하지 않으면 테스트를 통과한다.
@AllArgsConstructor
static class Parameter {
private String keyword;
}
@Test
public void notEqualsTest() {
Parameter parameter1 = new Parameter("우유");
Parameter parameter2 = new Parameter("우유");
assertNotEquals(parameter1, parameter2);
}
- assertTrue, assertFalse
- assertNull, assertNotNull
@AllArgsConstructor
static class Parameter extends Object {
private String keyword;
}
@Test
public void equalsTest() {
String failMessage = "객체의 equals와 hashCode를 재정의해야 합니다.";
Parameter parameter1 = new Parameter("우유");
Parameter parameter2 = new Parameter("우유");
assertEquals(parameter1, parameter2, failMessage);
}
2-3. exception
assertThrows
- 예외 발생 테스트
- 예외가 발생해야 테스트를 통과함.
public String getSecondArgument(String[] args) { return args[1]; } @Test public void throwExceptionTest() { String[] array = {"1"}; assertThrows(ArrayIndexOutOfBoundsException.class, () -> { getSecondArgument(array); }); }
-
- 가능하면 발생할 것으로 예상되는 exception 클래스를 직접 사용하는 것이 좋겠음.주의사항
- exception의 super class로 예외를 체크하는 경우 예상하지 않은 예외도 같이 통과할 수도 있음.
- 참고) Throwable 클래스와 예외 클래스 상속 구조
- 가능하면 발생할 것으로 예상되는 exception 클래스를 직접 사용하는 것이 좋겠음.주의사항
2-4. @DisplayName
- 기본적으로 테스트 결과에 표시되는 이름은 메소드명으로 설정되어 있음.
- 테스트 메소드에 @DisplayName를 사용하면 테스트 결과에 표시되는 테스트 이름을 바꿀수 있음.
@DisplayName("1. 서로 다른 객체가 가진 데이터가 같은 경우 동일한 객체로 판단한다. \uD83D\uDE4F")
@Test
public void equalsTest() {
String failMessage = "객체의 equals와 hashCode를 재정의해야 합니다.";
Parameter parameter1 = new Parameter("우유");
Parameter parameter2 = new Parameter("우유");
assertEquals(parameter1, parameter2, failMessage);
}
- 메소드명 정의 규칙과 상관없이 자유로운 방식으로 출력값을 지정할 수 있음.
2-5. 파라미터 테스트
- 단위 테스트 시 여러 케이스의 인자로 동일한 메소드를 테스트를 하는 경우, 비효율적임.
//검색어가 url 인코딩된 값이 아닌가?
public boolean isNotUrlEncoded(Parameter parameter) {
String keyword = parameter.getKeyword();
return !keyword.matches("^%\\w{2}%\\w{2}");
}
@Test
public void is_url_encoded_test(){
Parameter parameter = new Parameter("우유");
assertTrue(isNotUrlEncoded(parameter));
parameter = new Parameter("양파");
assertTrue(isNotUrlEncoded(parameter));
parameter = new Parameter("cucumber");
assertTrue(isNotUrlEncoded(parameter));
//우유 키워드를 url 인코딩
parameter = new Parameter("%EC%9A%B0%EC%9C%A0");
assertTrue(isNotUrlEncoded(parameter));
}
- gradle 의존성
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.1'
}
- @Test 어노테이션 대신 @ParameterizedTest 어노테이션을 사용하면 활성화됨.
@ValueSource
- 파라미터를 사용한 가장 기본적인 형태
- string, class 타입을 포함한 원시 타입 변수만 사용 가능
@ValueSource(strings = {"우유", "양파", "cucumber", "%EC%9A%B0%EC%9C%A0"})
@ParameterizedTest
public void is_url_encoded_test(String keyword) {
Parameter parameter = new Parameter(keyword);
assertTrue(isNotUrlEncoded(parameter));
}
@NullSource
- string 파라미터에 null 값 케이스를 추가한다.
@NullSource
@ValueSource(strings = {"우유", "양파", "cucumber", "%EC%9A%B0%EC%9C%A0"})
@ParameterizedTest
public void is_url_encoded_test(String keyword) {
Parameter parameter = new Parameter(keyword);
assertTrue(isNotUrlEncoded(parameter));
}
@EmptySource
- string 파라미터에 빈값 케이스를 추가한다.
@EmptySource
@ValueSource(strings = {"우유", "양파", "cucumber", "%EC%9A%B0%EC%9C%A0"})
@ParameterizedTest
public void is_url_encoded_test(String keyword) {
Parameter parameter = new Parameter(keyword);
assertTrue(isNotUrlEncoded(parameter));
}
@NullAndEmptySource
- string 파라미터에 빈값과 null 값 케이스를 각각 추가한다.
@NullAndEmptySource
@ValueSource(strings = {"우유", "양파", "cucumber", "%EC%9A%B0%EC%9C%A0"})
@ParameterizedTest
public void is_url_encoded_test(String keyword) {
Parameter parameter = new Parameter(keyword);
assertTrue(isNotUrlEncoded(parameter));
}
@EnumSource
- Enum의 모든 상수를 파라미터로 각각 추가한다.
- value에 enum class 추가
public enum Category {
신선식품("01"),
가전제품("02"),
잡화("03"),
여성옷("04"),
남성옷("05");
private final String ctgId;
Category(String ctgId) {
this.ctgId = ctgId;
}
}
@EnumSource(value = Category.class)
@ParameterizedTest
public void site_no_test(Category ctg) {
assertNotNull(ctg.ctgId);
}
- enum 특정 값을 포함, 미포함 조건으로 추가한다.
- mode, names에 들어간 값 기준으로 조회한다.
@EnumSource(mode = INCLUDE/*EXCLUDE*/, names = {"신선식품", "잡화"})
@ParameterizedTest
public void site_no_test(Category ctg) {
assertNotNull(ctg.ctgId);
}
- mode=MATCH_ANY, MATCH_ALL 로 정규식 조건으로 선택할 수도 있음.
@MethodSource
- 메소드 기반으로 파라미터값 사용 설정함.
- 인자가 한 개인 경우
- Stream에 테스트할 인자를 입력하면 순서대로 실행함.
@MethodSource("singleArgumentMethodSource")
@ParameterizedTest
public void single_argument_test(Category ctg) {
assertNotNull(ctg.ctgId);
}
static Stream<Site> singleArgumentMethodSource() {
return Stream.of(Category.신선식품, Category.잡화);
}
- 인자가 두 개 이상인 경우
- Stream 반환값 내부에서 Arguments.arguments로 여러 인자를 사용함.
- 시나리오 : 특정 브랜드 상품의 적정 가격 범위 이내인지 검증한다.
@AllArgsConstructor
public enum BrandItem {
APPLE_아이패드(500_000L, 1_500_000L);
private final long minPrice;
private final long maxPrice;
public static boolean isReasonablePrice(BrandItem brandItem, long price) {
return brandItem.minPrice <= price && brandItem.maxPrice >= price;
}
}
@MethodSource("multipleArgumentsMethodSource")
@ParameterizedTest
public void multiple_arguments_price_test(BrandItem brandItem, long price, boolean expected) {
assertEquals(expected, BrandItem.isReasonablePrice(brandItem, price));
}
static Stream<Arguments> multipleArgumentsMethodSource() {
return Stream.of(
arguments(BrandItem.APPLE_아이패드, 300_000, false),
arguments(BrandItem.APPLE_아이패드, 1_000_000, true),
arguments(BrandItem.APPLE_아이패드, 1_500_000, true)
);
}
@ArgumentsSource, @CsvSource, @CsvFileSource
- 굳이 사용할 것 같지는 않아서 생략.
3. AssertJ
- 단위테스트 보조 도구
- assertThat으로 시작하는 테스트 메소드를 사용할 수 있음.
- org.springframework.boot:spring-boot-starter-test 에서는 assertThat 메소드를 제공하는 hamcrest, assertj-core 모듈을 모두 포함하고 있음
- gradle 의존성
dependencies {
testImplementation 'org.assertj:assertj-core:3.18.0'
}
assertJ vs hamcrest
//Hamcrest 사용
assertThat(ratio, is(not(0.0)));
//AssertJ 사용
assertThat(ratio).isNotEqualTo(0.0);
- Hamcrest 사용시 Matcher 메소드를 알아야 쓸 수 있음.
- AssertJ 사용시 IDE에서 자동완성으로 쉽게 찾음.
- AssertJ 사용시 메서드 체이닝으로 여러 조건값을 동시에 모두 만족하는지 확인할 수 있음.
문자열 테스트
assertThat("Hello, world! Nice to meet you.") // 주어진 "Hello, world! Nice to meet you."라는 문자열은
.isNotEmpty() // 비어있지 않고
.contains("Nice") // "Nice"를 포함하고
.contains("world") // "world"도 포함하고
.doesNotContain("ZZZ") // "ZZZ"는 포함하지 않으며
.startsWith("Hell") // "Hell"로 시작하고
.endsWith("u.") // "u."로 끝나며
.isEqualTo("Hello, world! Nice to meet you."); // "Hello, world! Nice to meet you."과 일치합니다.
숫자 테스트
assertThat(3.14d) // 주어진 3.14라는 숫자는
.isPositive() // 양수이고
.isGreaterThan(3) // 3보다 크며
.isLessThan(4) // 4보다 작습니다
.isEqualTo(3, offset(1d)) // 오프셋 1 기준으로 3과 같고
.isEqualTo(3.1, offset(0.1d)) // 오프셋 0.1 기준으로 3.1과 같으며
.isEqualTo(3.14); // 오프셋 없이는 3.14와 같습니다
컬렉션 테스트
@Test
public void containsElementTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
assertThat(numbers).contains(12).doesNotContain(50);
}
@Test
public void containsAllElementsNoMatterOrderTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
List<Integer> values = Lists.newArrayList(52, 39, 12, 1, 100);
assertThat(numbers).containsAll(values);
}
@Test
public void containsAllElementsInOrderTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
List<Integer> values = Lists.newArrayList(numbers);
assertThat(numbers).containsExactlyElementsOf(values);
}
@Test
public void noDuplicatesTest(){
List<Integer> numbers = Lists.newArrayList(1, 52, 12, 39, 45, 98, 100, 565, 6, 13);
assertThat(numbers).doesNotHaveDuplicates();
}
@Test
public void containsOnlyOnceTest(){
List<Integer> numbers = Lists.newArrayList(1, 1, 52, 12, 12, 45, 45);
assertThat(numbers).containsOnlyOnce(52);q
}
- 여러 조건을 한번에 테스트 메소드로 체크하는 것이 유닛 테스트에 적합한 것인지는 모르겠음. 입력값에 대한 기대값을 임의로 검증하는 정도로 사용?
4. junit5 + mockito를 이용한 mock test
- 이상적으로, 각 테스트 케이스는 서로 분리되어야 한다. 이를 위해 가짜 객체(Mock object)를 생성하는 것도 좋은 방법이다.
Test Double
- 테스트 더블은 테스트를 진행하기 어려운 경우 이를 대신해 테스트를 진행할 수 있도록 만들어주는 객체를 말한다.
- 테스트 더블은 영화를 촬영할 때 배우를 대신하여 위험한 역할을 하는 스턴트 더블(Stunt Double)이라는 용어에서 유래된 단어이다.
- sociable test : 의존 관계가 간단한 경우 테스트 대상과 의존 대상을 함께 테스트 하는것
- solitary test : test double로 상호작용이 있는 유닛을 격리시켜 테스트하는 것
4-1 테스트 종류
- Dummy
- 인스턴스화된 객체가 필요하지만 기능은 필요하지 않은 경우 사용
public interface PringWarning { void print(); } public class PrintWarningDummy implements PrintWarning { @Override public void print() { // 아무런 동작을 하지 않는다. } }
- Fake
- 복잡한 로직이나 객체 내부에서 필요로 하는 다른 외부 객체들의 동작을 단순화하여 구현한 객체이다.
- 동작의 구현을 가지고 있지만 실제 프로덕션에 적합하진 않음.
public interface UserRepository { void save(User user); User findById(long id); } public class FakeUserRepository implements UserRepository { private Collection<User> users = new ArrayList<>(); @Override public void save(User user) { if (findById(user.getId()) == null) { users.add(user); } } @Override public User findById(long id) { for (User user : users) { if (user.getId() == id) { return user; } } return null; } }
- Stub
- Dummy 객체가 실제로 동작하는 것 처럼 보이게 만들어 놓은 객체
- 테스트에서 호출된 요청에 대해 미리 준비해둔 결과를 제공한다.
- 테스트가 수정될 경우 Stub 객체도 함께 수정해야 하는 단점 있음.
public class StubUserRepository implements UserRepository { // ... @Override public User findById(long id) { return **new User(id, "Test User"); // Stub 객체** } }
- Spy
- Stub의 역할을 갖고 호출된 내용에 대해 약간의 정보를 기록한다.
- 테스트 더블로 구현된 객체를 호출했을때 확인이 필요한 부분을 기록한다.
- 실제 객체처럼 쓰이거나, stub으로 동작을 지정할 수도 있다.
public class MailingService { private int sendMailCount = 0; private Collection<Mail> mails = new ArrayList<>(); public void sendMail(Mail mail) { sendMailCount++; mails.add(mail); } public long getSendMailCount() { return sendMailCount; } }
- Mockito의 verify() 메서드가 같은 역할
- Mock
- 호출에 대한 기대를 명세하고 내용에 따라 동작하도록 프로그래밍된 객체
- Mockito를 활용한 Mocking 코드 예시
@ExtendWith(MockitoExtension.class) public class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test void test() { when(userRepository.findById(anyLong())).thenReturn(new User(1, "Test User")); User actual = userService.findById(1); assertThat(actual.getId()).isEqualTo(1); assertThat(actual.getName()).isEqualTo("Test User"); } }
4-2 Mockito 사용법
- gradle 의존성
dependencies {
testImplementation 'org.mockito:mockito-junit-jupiter:4.8.0'
testImplementation 'org.mockito:mockito-core:4.8.0'
}
- 테스트 클래스 생성 (Mockito 관련 어노테이션이 붙은 객체가 있는 경우 필요함.)
@ExtendWith(MockitoExtension.class)
public class MockTest {
}
- Mockito.mock, @Mock
- mock 객체는 가짜 객체이며, 반드시 스터빙을 해야 하고 스터빙을 하지 않고 호출하면, 0이나 null값을 반환한다.
//선언 방법 1. @Mock private UserRepository userRepository; @Test public void test(){ //선언 방법 2. UserRepository userRepository = Mockito.mock(UserRepository.class); }
- Mockito.spy, @Spy
- 객체의 타입이 인터페이스인 경우에는 @Mock 객체와 동작이 같음.
- 객체의 타입이 클래스인 경우
- 스터빙을 하지 않은 경우 해당 객체의 실제 코드를 실행.
- 스터빙을 한 경우 스터빙한 값을 반환함.
//선언 방법 1. @Spy private UserRepository userRepository; public void test(){ //선언 방법 2. UserRepository userRepository = Mockito.spy(UserRepository.class); }
- @InjectMocks
- @Mock, @Spy로 생성한 mock 객체를 자동으로 주입해줌.
@Mock private AccountService accountService; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private LoginController loginController;
Stubbing
1. OngoingStubbing
- 스터빙 대상의 객체와 메소드를 입력받아 원하는 값이나 에러를 정의함.
- thenReturn
@DisplayName("동일한 유저를 가입시킬때 중복 가입 시도시 null을 반환한다.")
@Test
public void duplicateJoinTest() {
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
when(mockRepository.save(any()))
.thenReturn(user)
.thenThrow(DuplicateUserInfoException.class);
assertEquals(user, service.joinUser("랜디"));
assertNull(service.joinUser("랜디"));
}
- thenThrow
@DisplayName("동일한 유저를 가입시킬때 중복 가입 시도시 null을 반환한다.")
@Test
public void duplicateJoinTest() {
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
when(mockRepository.save(any()))
.thenReturn(user)
.thenThrow(DuplicateUserInfoException.class);
assertEquals(user, service.joinUser("랜디"));
assertNull(service.joinUser("랜디"));
}
- 메서드 체이닝으로 동일한 호출을 할 경우 호출마다 다른 동작을 하게 할 수 있음.
- 주의사항 : 인터페이스가 아닌 클래스의 객체 Spy 사용시 when 절에서 메소드를 직접 실행시킴. 이를 방지하려면 OnGoingStubbing이 아닌 Stubber를 사용해야 한다.
2. Stubber
- 반환값을 미리 넣어놓고 이후 when 메소드에 Mock 객체와 동작시킬 메소드를 적는 방식
- void를 반환하는 메소드도 테스트가 가능함.
- doReturn
@Mock
private UserRepository mockRepository;
@InjectMocks
UserService service;
@DisplayName("유저 ID로 검색을 호출했을 때 정상적으로 유저를 반환한다.")
@Test
public void mockTest() {
User user = new User(1L, "랜디");
when(mockRepository.findById(1L)).thenReturn(user);
User savedUser = service.findUser(1L);
assertEquals(user, savedUser);
}
- doThrow
@DisplayName("유저 ID로 검색을 호출했을 때 유저가 없는 경우 게스트 유저를 반환한다.")
@Test
public void throwTest() {
User user = new User(0L, "guest");
UserService service = new UserService(mockRepository);
doThrow(UserNotFoundException.class).when(mockRepository).findById(1L);
assertEquals(user, service.findUser(1L));
}
- doNothing
@DisplayName("유저가 로그인을 한 후, 유저의 총 로그인 횟수를 체크한다.")
@Test
public void doNothingTest() {
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
doNothing().when(mockRepository).loggingLogin(user);
service.loggingLogin(user);
assertEquals(1, service.getTotalLoginCount());
}
Verify
- verify(T mock, VerificationMode mode)
- VerificationMode
메소드명 설명 times(n) 몇 번이 호출됐는지 검증 never 한 번도 호출되지 않았는지 검증 atLeastOne 최소 한 번은 호출됐는지 검증 atLeast(n) 최소 n 번이 호출됐는지 검증 atMostOnce 최대 한 번이 호출됐는지 검증 atMost(n) 최대 n 번이 호출됐는지 검증 … @DisplayName("유저 가입 요청이 들어왔을때, 기존에 가입된 정보가 있는지 확인하고 가입 후 곧바로 로그인시킨다.") @Test public void loginTest() { User user = new User(1L, "랜디"); UserService service = new UserService(mockRepository); when(mockRepository.findById(1L)).thenReturn(null); when(mockRepository.save(user)).thenReturn(user); doNothing().when(mockRepository).loggingLogin(user); service.joinAndLogin(user); assertEquals(1, service.getTotalLoginCount()); verify(mockRepository, times(1)).findById(1L); verify(mockRepository, times(1)).save(user); verify(mockRepository, times(1)).loggingLogin(user); }
InOrder
- 객체와 서로 다른 메소드 실행 순서가 일치하는지 테스트할 수 있음.
@Test
public void inOrderLoginTest() {
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
when(mockRepository.findById(1L)).thenReturn(null);
when(mockRepository.save(user)).thenReturn(user);
doNothing().when(mockRepository).loggingLogin(user);
InOrder inOrder = inOrder(mockRepository);
service.joinAndLogin(user);
assertEquals(1, service.getTotalLoginCount());
inOrder.verify(mockRepository, times(1)).findById(1L);
inOrder.verify(mockRepository, times(1)).save(user);
inOrder.verify(mockRepository, times(1)).loggingLogin(user);
}
Mock 남용시 단점
- 링크 참고
- 요약 : 테스트 관리비용이 높아짐, 이해하기 어려워짐, 실제 구현체를 쓰지않아 신뢰도가 떨어짐.
기타
BDD 스타일의 Mockito
- given, when, then : 시나리오 기반으로 테스트 코드를 표현하는 스타일
- given : 테스트에서 구체화하고자 하는 행동을 시작하기 전에 테스트 상태를 설명하는 부분
- when : 구체화하고자 하는 그 행동
- then : 어떤 특정한 행동 때문에 발생할거라고 예상되는 변화에 대한 설명
예시
기능 : 사용자 주식 트레이드
시나리오 : 트레이드가 마감되기 전에 사용자가 판매를 요청
"Given" 나는 마이크로소프트 주식을 100가지고 있다.
그리고 나는 애플 주식을 150가지고 있다.
그리고 시간은 트레이드가 종료되기 전이다.
"When" 나는 마이크로소프트 주식 20을 팔도록 요청했다.
"Then" 나는 마이크로소프트 주식 80 가지고 있어야 한다.
그리고 나는 애플 주식 150을 가지고 있어야 한다.
그리고 마이크로소프트 주식 20이 판매 요청이 실행되었어야 한다.
//mockito를 사용해 BDD 형식으로 작성시 혼동이 좀 생김
@DisplayName("유저 가입 요청이 들어왔을때, 기존에 가입된 정보가 있는지 확인하고 가입 후 곧바로 로그인시킨다.")
@Test
public void mockitoLoginTest() {
//given
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
when(mockRepository.findById(1L)).thenReturn(null);
when(mockRepository.save(user)).thenReturn(user);
doNothing().when(mockRepository).loggingLogin(user);
//when
service.joinAndLogin(user);
//then
assertEquals(1, service.getTotalLoginCount());
verify(mockRepository, times(1)).findById(1L);
verify(mockRepository, times(1)).save(user);
verify(mockRepository, times(1)).loggingLogin(user);
}
- BDDMockito 사용시 BDD 스타일로 가독성이 좋아졌다.
//mockito를 사용해 BDD 형식으로 작성시 혼동이 좀 생김
@DisplayName("유저 가입 요청이 들어왔을때, 기존에 가입된 정보가 있는지 확인하고 가입 후 곧바로 로그인시킨다.")
@Test
public void bddMockitoLoginTest() {
//given
User user = new User(1L, "랜디");
UserService service = new UserService(mockRepository);
given(mockRepository.findById(1L)).willReturn(null);
given(mockRepository.save(user)).willReturn(user);
doNothing().when(mockRepository).loggingLogin(user);
//when
service.joinAndLogin(user);
//then
assertEquals(1, service.getTotalLoginCount());
then(mockRepository).should(times(1)).findById(1L);
then(mockRepository).should(times(1)).save(user);
then(mockRepository).should(times(1)).loggingLogin(user);
}
참고자료
- https://ko.wikipedia.org/wiki/유닛_테스트
- https://www.daleseo.com/assertj/
- https://dev.to/iuriimednikov/common-assertions-for-java-collections-in-assertj-4h3g
- https://tecoble.techcourse.co.kr/post/2020-09-19-what-is-test-double/
- https://medium.com/@chanhyeonglee/mock-객체-남용은-테스트-코드를-망친다-f38129e5d40a
'개발 > java' 카테고리의 다른 글
[java] 람다와 클로저 (0) | 2024.01.28 |
---|---|
[java] IntelliJ 자바 개발시 애플리케이션 성능 높이는 방법 (0) | 2024.01.26 |