본문 바로가기
개발/Test

[Test] Mockito를 사용해보자 (1)

by solchan98 2022. 2. 10.

Mockito

자바 코드를 테스트는 JUnit5 프레임워크를 통해 진행할 수 있다.
그럼 Mockito는 뭔가?

우선 Mock의 뜻은 모조품이다.
즉, Mockito는 가짜 객체를 만들 수 있도록 해준다.

Mock이 필요한 이유

그럼 왜 가짜 객체가 필요한가?

예를 들어, 나는 B라는 객체의 run()메서드를 테스트 하고 싶다. 그런데 B 인스턴스를 생성하기 위해서는 A라는 인스턴스를 B의 생성자 매개변수로 넘겨주어야 한다.
즉, B가 A에 의존하고 있는 것을 알 수 있다.
만약, A가 또 다른 객체에 의존하고 있다면 B의 run()메서드를 테스트 하기 위해 많은 인스턴스를 생성해야 한다.

이렇게 강한 의존성을 끊기위해 가짜 객체를 생성하고 이를 Mockito를 통해 사용할 수 있다.

또, 구현체가 없는 인터페이스를 사용하려면 다음과 같이 테스트 코드 안에서 구현을 하여야한다.
만약 스프링 프로젝트라면 테스트에서는 컨테이너에서 빈 주입을 받을 수 없다. (@SpringBootTest 사용하면 가능)

    @Test
    @DisplayName("계정 정보로 자동차 찾기")
    void findByAccountTest() {
        CarService carService = new CarService() {
            @Override
            public Account findByAccount(Account account) {
                return null;
            }
        };
    }

하지만, 가짜 객체를 만들어 해당 객체의 행동까지 우리가 원하는 대로 조작할 수 있다.

테스트 환경

테스트 환경은 다음과 같다.

Test Environment
Platform: SpringBoot
IDE: IntelliJ
Java version: 11
Test Framwork: JUnit5, Mockito
Gradle

스프링부트 프로젝트를 생성하면 기본적으로 boot-starter에 mockito가 포함되어 있다.
만약 스프링부트 프로젝트가 아닌 Java | Gradle로 만들었다면 mockito를 추가해주면 된다.

가짜객체 만들기

시나리오

테스트를 진행하기위해 시나리오를 다음과 같이 구성하였다.
Gym, Account도메인이 있고 GymService는 AccountService와 GymRepository를 필요로 한다.
AccountService와 GymRepository는 인터페이스로만 작성되어있다.

// Gym, Account 도메인
public class Account {
    private String name;
    private Long age;

    public Account(String name, Long age) {
        this.name = name;
        this.age = age;
    }
}

public class Gym {
    private String name;
    private List<Account>  memberList = new ArrayList<>();

    public Gym(String name) {
        this.name = name;
    }

    public void setMember(Account account) {
        this.memberList.add(account);
    }

    public List<Account> getMemberList() {
        return this.memberList;
    }
}
// AccountService, GymRepository interface
public interface AccountService {
    Account findByName(String name);
}

@NoRepositoryBean
public interface GymRepository extends JpaRepository<Gym, Long> {
}
// GymService
public class GymService {
    private final AccountService accountService;
    private final GymRepository gymRepository;

    public GymService(AccountService accountService, GymRepository gymRepository) {
        this.accountService = accountService;
        this.gymRepository = gymRepository;
    }

    public Gym addAccount(String name, Gym gym) {
        Account account = accountService.findByName(name);
        gym.setMember(account);
        return gymRepository.save(gym);
    }
}

만약, 위 경우라면 GymService를 만들기 위해 AccountService와 GymRepository를 테스트 메서드 안에서 전부 구현해야한다.

하지만 JpaRepository를 받은 GymRepository를 구현하기에는 너무 많은 구현이 필요하다.

따라서 AccountService와 GymRepository를 가짜 객체로 만들어 사용하는 것이다.

mock()을 사용하여 mocking

import static org.mockito.Mockito.mock을 import하고, mock()을 통해서 테스트메서드 안에서 mocking을 할 수 있다.

assertNotNull을 통해 gymService가 null이 아닌 것을 확인할 수 있다.

    @Test
    @DisplayName("mock()")
    void mockInMethod() {
        AccountService accountService = mock(AccountService.class);
        GymRepository gymRepository = mock(GymRepository.class);
        GymService gymService = new GymService(accountService, gymRepository);
        assertNotNull(gymService);
    }

@Mock을 사용하여 mocking

@Mock를 사용하기 위해서는 MockitoExtension을 확장시켜줘야 한다.
@ExtendWith을 통해 MockitoExtension을 추가해준다.
@Mock을 사용하면 필드에 선언하여 사용이 가능하다.

1. 필드에 선언하여 사용하기

@ExtendWith(MockitoExtension.class)
class GymServiceTest {

    @Mock
    private AccountService accountService;

    @Mock
    private GymRepository gymRepository;

    @Test
    @DisplayName("@Mock - In Field")
    void mockByAnnotationInField() {
        GymService gymService = new GymService(accountService, gymRepository);
        assertNotNull(gymService);
    }
}

2. 메서드 매개변수로 받아 사용하기
만약, Mock으로 만들어 사용할 객체가 해당 테스트 메서드에서만 쓰이는 경우라면 필드에 선언할 필요는 없다. 이런 경우에는 메서드의 매개변수로 받아 사용이 가능하다.

@ExtendWith(MockitoExtension.class)
class GymServiceTest {
    @Test
    @DisplayName("@Mock - In Method")
    void mockByAnnotationInMethod(@Mock AccountService accountService, @Mock GymRepository gymRepository) {
        GymService gymService = new GymService(accountService, gymRepository);
        assertNotNull(gymService);
    }
}

Mock객체 Stubbing하기

이제 accountService와 gymRepository 가짜 객체를 만들어 gymService를 만들었다.
그러면 gymService의 메서드를 호출하면 정상적으로 수행이 될까?
그렇지 않다.

생성된 Mock객체는 다음의 특성을 갖는다.

  1. 모든 수행 결과는 null을 리턴한다.(Optional은 empty)
  2. 컬렉션은 비어있는 상태이다.

따라서 우리는 생성한 두 Mock객체를 Stubbing해야 한다.

GymSerivce의 addAccount메서드를 호출하면 accountService의 findByName()을 호출한다. 최종적으로 Account를 리턴받는다.

하지만 accountService는 구현체가 아닌 인터페이스라 실제 수행 로직이 존재하지 않는다.
만약, 스프링 프로젝트라면 테스트를 진행할 때 컨테이너에 등록된 빈을 주입받을 수 없기 때문에 구현체가 없는 경우와 마찬가지다. (@SpringBootTest을 붙이면 가능하다.)
따라서 해당 메서드가 호출되면 응답하는 값을 우리가 직접 조작할 수 있다.

when().thenReturn();
when은 호출될 메서드를 넘겨주고, thenReturn은 해당 메서드가 호출되었을 때 리턴할 값을 넘겨준다.
즉, 어떤 메서드가 호출될 때, 어떤 값을 넘겨주는지 설정하는 것이다.

    @Test
    @DisplayName("헬스장에 회원 추가하기")
    void addMember() {
        Account account = new Account("SolChan", 24L);
        Gym gym = new Gym("헬스장");
        GymService gymService = new GymService(accountService, gymRepository);
        Mockito.when(accountService.findByName(any())).thenReturn(account);
        gymService.addAccount("SolChan", gym);
        assertEquals(account, gym.getMemberList().get(0));
    }

위 코드를 보면 accountService.findByName(...)가 호출되었을 때 account를 리턴해준다고 직접 설정을 하는 것이다.
any()는 매개변수에 뭐가 들어오든 account를 리턴한다는 뜻이다.
만약 any()가 아니라 특정 매개변수 값을 넣어준다면, 해당 매개변수가 들어왔을때만 account를 리턴한다.

위 예제는 간단한 경우이며 when().then().then()...이렇게 체닝이 가능하여 여러 조건을 설정할 수 있다.

'개발 > Test' 카테고리의 다른 글

[Test] Mockito를 사용해보자 (2)  (0) 2022.02.10
[Test] JUnit5을 사용해보자 (2)  (0) 2022.02.10
[Test] JUnit5을 사용해보자 (1)  (0) 2022.02.10