Mockito로 효율적으로 테스트하기

🔥Log·2024년 7월 17일

테스트

목록 보기
3/5

💡 글에서 사용된 코드: 깃헙

☕ 개요


이번 글에서는 Mockito를 활용해서 좀 더 효율적인 테스트 코드를 작성해보는 방법들에 대해서 알아보도록 하겠다.

Mockito 조차도 사용하지 않으면서 테스트 코드를 작성하는 것이 기술의 의존도를 줄이는 입장에서 가장 이상적이긴 하지만, 테스트 코드 작성에 대한 효율성도 생각한다면, Mockito를 사용해서 Mocking, Stubbing 등등을 하는 것도 좋은 방법이라고 생각한다.

그렇다면, 바~로 ㄱㄱ



✍️ 사전 준비


Mockito를 사용하기 위해서 객체, Repository, Service들을 만들어주도록 하겠다.

0) build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

참고삼아서, 나의 build.gradle을 공유한다. 꼭 이렇게 해줄 필요는 없고, JPA랑 JUnit5만 쓸 수 있으면 된다.

1) 객체 만들기

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@Entity
@NoArgsConstructor
@AllArgsConstructor
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;

}

사람을 나타내는 클래스를 하나 만들어준다.

2) Repository 만들기

import org.springframework.data.jpa.repository.JpaRepository;

public interface PersonJpaRepository extends JpaRepository<Person, Long> {}

JpaRepository를 상속받아서 Repository를 뚝딱 만들어준다.

3) Service 인터페이스 만들기

public interface PersonService {

    Person create(Person person);

    Person update(Long id, String name);

    void delete(Long id);

    Person getOne(Long id);

}

인터페이스를 만드는 것이 필수는 아니지만, 테스트 목적으로 여러 서비스 클래스를 구현할 것이므로, 통일성을 위해서 인터페이스를 만들어주었다.

Person을 CRUD할 수 있는 단순한 기능들이 구현될 예정이다.

4) PersonServiceImpl 만들기

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class PersonServiceImpl implements PersonService {

    private final PersonJpaRepository personJpaRepository;

    public Person create(Person person) {
        return personJpaRepository.save(person);
    }

    public Person update(Long id, String name) {
        Person person = personJpaRepository.findById(id).orElse(null);
        if (person == null) return null;

        person.setName(name);
        return personJpaRepository.saveAndFlush(person);
    }

    public void delete(Long id) {
        personJpaRepository.deleteById(id);
    }

    public Person getOne(Long id) {
        return personJpaRepository.findById(id).orElse(null);
    }
}

PersonJpaRepository를 사용하는 PersonService의 구현체를 만들어준다.

5) PersonSpyService 만들기

public class PersonSpyService implements PersonService {

    @Override
    public Person create(Person person) {
        return new Person(1L, "Kim_create", "hello@world.com");
    }

    @Override
    public Person update(Long id, String name) {
        return new Person(2L, "Kim_update", "hello@world.com");
    }

    @Override
    public void delete(Long id) {
        // do nothing
    }

    @Override
    public Person getOne(Long id) {
        return new Person(3L, "Kim_getOne", "hello@world.com");
    }
}

아래에서 @Spy를 테스트할 때 사용할 클래스를 하나 만들어준다.

6) 메모리 서비스 만들기

import java.util.ArrayList;
import java.util.List;

public class FakePersonService implements PersonService {

    List<Person> persons = new ArrayList<>();

    @Override
    public Person create(Person person) {
        Long newId = Long.valueOf(persons.size() + 1);
        person.setId(newId);
        persons.add(person);
        return person;
    }

    @Override
    public Person update(Long id, String name) {
        Person person = persons.stream().filter(p -> p.getId().equals(id)).toList().getFirst();
        if (person != null) {
            person.setName(name);
            return person;
        }
        return null;
    }

    @Override
    public void delete(Long id) {
        persons.removeIf(p -> p.getId().equals(id));
    }

    @Override
    public Person getOne(Long id) {
        return persons.stream().filter(p -> p.getId().equals(id)).toList().getFirst();
    }
}

메모리에만 데이터를 저장하고 조회할 수 있는 서비스를 만들어준다.
아래 테스트에서 사용될 예정이다.



🧐 Mockito 알아보기


1) @Mock

@Mock
PersonJpaRepository mockPersonJpaRepository;

@Mock은 클래스에 붙여서 사용하며, 껍데기만 있는 클래스를 만들어준다.

Mocking된 클래스에 있는 메소드를 사용하려면, Mockito.when()을 사용해서 반드시 Stubbing해줘야한다. 그렇지 않으면, 에러가 난다.

2) @Spy

@Spy
PersonSpyService personSpyService;

@Spy도 클래스에 붙여서 사용한다. @Mock을 사용할 때는 사용할 메서드를 반드시 Stubbing해줘야하지만, @Spy가 붙은 클래스에서는 Stubbing되지 않은 메서드가 호출된다면, 원래 메서드가 그대로 호출이 된다.

즉, Stubbing된 것은 Stubbing한 로직이 동작하고 그렇지 않은 것은 원래 메서드가 동작한다.

3) @InjectMocks

@InjectMocks
PersonServiceImpl personService;

@InjectMocks 역시 클래스에 붙여서 사용한다.

해당 클래스에서 의존성 주입을 받고 있는 것이 있다면, @Mock 또는 @Spy가 붙어 있는 클래스가 주입된다.

즉, Mock 또는 Spy 클래스를 주입하고 싶다면, @InjectMocks를 사용하면 된다.

4) verify()

Mockito.verify({클래스}).{메서드}

.verify() 메서드는 특정 메서드가 호출되었는지, 메서드가 호출되면서 특정 매개변수가 사용됐는지 확인할 수 있는 메서드이다.

보통은 "특정 Input이 들어갔을 때, Output이 이럴 것이다"라는 기조로 테스트 코드가 작성되는데, .verify()를 사용하면 로직의 내부 동작을 테스트할 수 있게 된다.

5) @Captor

@Captor
ArgumentCaptor<Long> longCaptor;

@Captor도 역시 클래스에 붙여서 사용하고, .verify()와 함께 사용될 수 있다.

.verify()에서 검증될 메서드에 넘어가는 매개변수를 Captor가 가져올 수 있고, 이 값에 대한 검증 또한 구현할 수 있다.



💻 Mockito로 테스트코드 작성하기


0) MockitoExtension

@ExtendWith(MockitoExtension.class)
class PersonServiceTest {
    ...
}

Mockito로 테스트를 수행하려면, 위와 같이 MockitoExtenson을 선언해줘야한다. 자세한 내용은 Baeldung 글을 참고하자 👍

1) Mock 테스트

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

@Slf4j
@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    final Person dummyPerson = new Person(999L, "DummyPerson", "person@dummy.com");

    @InjectMocks
    PersonServiceImpl personService;

    @Mock
    PersonJpaRepository mockPersonJpaRepository;

    @Test
    void Mock_테스트() {
        when(mockPersonJpaRepository.findById(1L)).thenReturn(Optional.of(dummyPerson));

        // Mock 검증
        Person person = personService.getOne(1L);
        assertEquals(999L, person.getId());
        assertEquals("DummyPerson", person.getName());
    }

}

@Mock을 붙인 mockPersonJpaRepository.getOne()을 Mocking하였다.
Id를 1L로 조회했을 때, dummyPerson이 Return되도록 Stubbing을 한 테스트 코드이다.

이 테스트가 통과하면, 정상적으로 Mocking이 된 것을 확인할 수 있다.

2) Spy 테스트

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

@Slf4j
@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    final Person dummyPerson = new Person(999L, "DummyPerson", "person@dummy.com");

    @Spy
    PersonSpyService personSpyService;
    
    @Test
    void Spy_테스트() {
        when(personSpyService.getOne(1L)).thenReturn(dummyPerson);

        // Spy 검증
        Person mustBeSpy = personSpyService.getOne(1L);
        assertEquals(999L, mustBeSpy.getId());
        assertEquals("DummyPerson", mustBeSpy.getName());

        // 안 Spy 검증
        Person mustNotBeSpy = personSpyService.getOne(2L);
        assertEquals(3L, mustNotBeSpy.getId());
        assertEquals("Kim_getOne", mustNotBeSpy.getName());
    }

}

Stubbing을 한 경우, Stubbing한 결과가 Return되고 그렇지 않은 경우 정상적인 로직이 수행되어 정상적인 결과가 Return되는지 확인하는 테스트 코드이다.

3) verify() 테스트

import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

@Slf4j
@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    @Mock
    FakePersonService fakePersonService;

    @Test
    void Verify_테스트() throws Exception {
        fakePersonService.delete(999L);

        verify(fakePersonService).delete(999L);
        verify(fakePersonService).delete(anyLong());
    }

}

fakePersonService.delete(999L);를 실행하고 .delete()에 사용된 매개변수가 999L인지, Long타입의 매개변수가 사용됐는지 확인하는 테스트 코드이다.

눈으로도 확인할 수 있는 수준의 이상한 테스트이지만, .verify()가 어떤 역할로 사용될 수 있는지만 파악하자 😅

4) Captor 테스트

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;

import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

@Slf4j
@ExtendWith(MockitoExtension.class)
class PersonServiceTest {

    @Mock
    FakePersonService fakePersonService;

    @Captor
    ArgumentCaptor<Long> longCaptor;

    @Test
    void Captor_테스트() {
        Long randomId = List.of(1L, 2L, 3L, 4L).get((int) (Math.random() * 4));
        fakePersonService.delete(randomId);

        verify(fakePersonService).delete(longCaptor.capture());

        Long selectedId = longCaptor.getValue();

        assertEquals(randomId, selectedId);
    }

}

이것도 좀 이상한 테스트이지만, randomId를 통해서 .delete() 메서드를 동작시켰고, longCaptor가 매개변수를 가져와서 실제 들어간 값과 Captor가 가져온 값이 일치하는지 테스트하는 코드이다.

좀 이상한 테스트이지만, '아 저렇게 사용할 수 있구나' 정도만 알면 될 것 같다. 😅

5) 테스트 결과

테스트는 모두 잘 통과하였고, 각 기능들이 정상적으로 동작했다는 것을 확인할 수 있다. 👍



🙏 참고


0개의 댓글