1. 개요

Java 8은 람다 및 스트림과 같은 새롭고 멋진 기능을 다양하게 도입했습니다. 그리고 당연히 Mockito는 두 번째 주요 버전 에서 이러한 최신 혁신을 활용했습니다 .

이 기사에서는 이 강력한 조합이 제공하는 모든 것을 살펴보겠습니다.

2. 기본 메서드를 사용한 모의 인터페이스

Java 8부터는 이제 인터페이스에서 메서드 구현을 작성할 수 있습니다. 이것은 훌륭한 새 기능일 수 있지만 언어에 대한 소개는 개념이 시작된 이후 Java의 일부였던 강력한 개념을 위반했습니다.

Mockito 버전 1은 이 변경 사항에 대해 준비되지 않았습니다. 기본적으로 인터페이스에서 실제 메서드를 호출하도록 요청할 수 없었기 때문입니다.

2개의 메서드 선언이 있는 인터페이스가 있다고 상상해 보십시오. 첫 번째는 우리 모두에게 익숙한 구식 메서드 서명이고 다른 하나는 완전히 새로운 기본 메서드입니다.

public interface JobService {
 
    Optional<JobPosition> findCurrentJobPosition(Person person);
    
    default boolean assignJobPosition(Person person, JobPosition jobPosition) {
        if(!findCurrentJobPosition(person).isPresent()) {
            person.setCurrentJobPosition(jobPosition);
            
            return true;
        } else {
            return false;
        }
    }
}

assignJobPosition () 기본 메서드에는 구현되지 않은 findCurrentJobPosition() 메서드에 대한 호출이 있습니다.

이제 실제 findCurrentJobPosition () 구현을 작성하지 않고 assignJobPosition( ) 구현 을 테스트하고 싶다고 가정합니다 . 간단히 JobService 의 모의 버전을 만든 다음 Mockito에게 구현되지 않은 메서드 호출에서 알려진 값을 반환하고 assignJobPosition()이 호출될 때 실제 메서드를 호출하도록 지시할 수 있습니다.

public class JobServiceUnitTest {
 
    @Mock
    private JobService jobService;

    @Test
    public void givenDefaultMethod_whenCallRealMethod_thenNoExceptionIsRaised() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(person))
              .thenReturn(Optional.of(new JobPosition()));

        doCallRealMethod().when(jobService)
          .assignJobPosition(
            Mockito.any(Person.class), 
            Mockito.any(JobPosition.class)
        );

        assertFalse(jobService.assignJobPosition(person, new JobPosition()));
    }
}

이것은 완벽하게 합리적이며 인터페이스 대신 추상 클래스를 사용하고 있다는 점에서 잘 작동합니다.

그러나 Mockito 버전 1의 내부 작업은 이 구조에 대해 준비되지 않았습니다. Mockito 이전 버전 2에서 이 코드를 실행하면 다음과 같이 잘 설명된 오류가 발생합니다.

org.mockito.exceptions.base.MockitoException:
Cannot call a real method on java interface. The interface does not have any implementation!
Calling real methods is only possible when mocking concrete classes.

Mockito는 작업을 수행하고 있으며 Java 8 이전에는 이 작업을 생각할 수 없었기 때문에 인터페이스에서 실제 메서드를 호출할 수 없다고 말합니다.

좋은 소식은 우리가 사용하고 있는 Mockito의 버전을 변경하는 것만으로도 이 오류를 해결할 수 있다는 것입니다. 예를 들어 Maven을 사용하면 버전 2.7.5를 사용할 수 있습니다(최신 Mockito 버전은 여기에서 찾을 수 있음 ).

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.7.5</version>
    <scope>test</scope>
</dependency>

코드를 변경할 필요가 없습니다. 다음에 테스트를 실행할 때 오류가 더 이상 발생하지 않습니다.

3. 선택적스트림 에 대한 기본값 반환

선택적 스트림은 다른 Java 8의 새로운 추가 사항입니다. 두 클래스 사이의 한 가지 유사점은 둘 다 빈 객체를 나타내는 특별한 유형의 값을 가지고 있다는 것입니다. 이 빈 개체는 지금까지 편재하는 NullPointerException 을 쉽게 피할 수 있습니다 .

3.1. 옵션이 있는 예

이전 섹션에서 설명한 JobService를 주입하고 JobService#findCurrentJobPosition()을 호출하는 메서드가 있는 서비스를 고려하십시오 .

public class UnemploymentServiceImpl implements UnemploymentService {
 
    private JobService jobService;
    
    public UnemploymentServiceImpl(JobService jobService) {
        this.jobService = jobService;
    }

    @Override
    public boolean personIsEntitledToUnemploymentSupport(Person person) {
        Optional<JobPosition> optional = jobService.findCurrentJobPosition(person);
        
        return !optional.isPresent();
    }
}

이제 어떤 사람이 현재 직책이 없을 때 실업 지원을 받을 자격이 있는지 확인하는 테스트를 만들고 싶다고 가정합니다.

이 경우 findCurrentJobPosition()이옵션을 반환하도록 강제합니다 . Mockito 버전 2 이전에는 해당 메서드에 대한 호출을 조롱해야 했습니다.

public class UnemploymentServiceImplUnitTest {
 
    @Mock
    private JobService jobService;

    @InjectMocks
    private UnemploymentServiceImpl unemploymentService;

    @Test
    public void givenReturnIsOfTypeOptional_whenMocked_thenValueIsEmpty() {
        Person person = new Person();

        when(jobService.findCurrentJobPosition(any(Person.class)))
          .thenReturn(Optional.empty());
        
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

13행의 when (…).thenReturn(…) 명령어는 모의 객체에 대한 모든 메서드 호출에 대한 Mockito의 기본 반환 값이 null 이기 때문에 필요합니다 . 버전 2는 그 동작을 변경했습니다.

Optional을 처리할 때 null 값을 거의 처리하지 않기 때문에 이제 Mockito는 기본적으로 Optional을 반환합니다 . 이는 Optional.empty() 에 대한 호출의 반환과 정확히 동일한 값입니다 .

따라서 Mockito 버전 2를 사용할 때 13행을 제거해도 테스트는 여전히 성공적일 것입니다.

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsOptional_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();
 
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(person));
    }
}

3.2. 스트림 의 예

Stream 을 반환하는 메서드를 조롱할 때도 동일한 동작이 발생합니다 .

한 사람이 지금까지 일한 모든 직책을 나타내는 Stream을 반환하는 JobService 인터페이스 에 새 메서드를 추가해 보겠습니다 .

public interface JobService {
    Stream<JobPosition> listJobs(Person person);
}

이 메서드는 사람이 주어진 검색 문자열과 일치하는 작업에서 일한 적이 있는지 쿼리하는 또 다른 새 메서드에서 사용됩니다.

public class UnemploymentServiceImpl implements UnemploymentService {
   
    @Override
    public Optional<JobPosition> searchJob(Person person, String searchString) {
        return jobService.listJobs(person)
          .filter((j) -> j.getTitle().contains(searchString))
          .findFirst();
    }
}

따라서 listJobs () 작성에 대해 걱정할 필요 없이 searchJob () 의 구현을 적절하게 테스트하고 싶다고 가정 하고 사람이 아직 어떤 작업도 하지 않았을 때 시나리오를 테스트하고 싶다고 가정합니다. 이 경우 listJobs()가 빈 Stream을 반환하기를 원할 것입니다 .

Mockito 버전 2 이전에는 이러한 테스트를 작성하기 위해 listJobs() 에 대한 호출을 조롱해야 했습니다 .

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsOfTypeStream_whenMocked_thenValueIsEmpty() {
        Person person = new Person();
        when(jobService.listJobs(any(Person.class))).thenReturn(Stream.empty());
        
        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

버전 2로 업그레이드하면 when(…).thenReturn(…) 호출을 삭제할 수 있습니다 . 이제 Mockito가 기본적으로 조롱된 메서드에서 스트림을 반환하기 때문입니다 .

public class UnemploymentServiceImplUnitTest {
 
    @Test
    public void givenReturnIsStream_whenDefaultValueIsReturned_thenValueIsEmpty() {
        Person person = new Person();
        
        assertFalse(unemploymentService.searchJob(person, "").isPresent());
    }
}

4. 람다 식 활용

Java 8의 람다 식을 사용하면 명령문을 훨씬 더 간결하고 읽기 쉽게 만들 수 있습니다. Mockito로 작업할 때 람다 식으로 가져온 단순성의 두 가지 아주 좋은 예는 ArgumentMatchers 및 사용자 정의 Answers 입니다 .

4.1. Lambda와 ArgumentMatcher 의 조합

Java 8 이전에는 ArgumentMatcher 를 구현하는 클래스를 만들고 matches() 메서드 에 사용자 정의 규칙을 작성 해야 했습니다 .

Java 8에서는 내부 클래스를 간단한 람다 식으로 바꿀 수 있습니다.

public class ArgumentMatcherWithLambdaUnitTest {
 
    @Test
    public void whenPersonWithJob_thenIsNotEntitled() {
        Person peter = new Person("Peter");
        Person linda = new Person("Linda");
        
        JobPosition teacher = new JobPosition("Teacher");

        when(jobService.findCurrentJobPosition(
          ArgumentMatchers.argThat(p -> p.getName().equals("Peter"))))
          .thenReturn(Optional.of(teacher));
        
        assertTrue(unemploymentService.personIsEntitledToUnemploymentSupport(linda));
        assertFalse(unemploymentService.personIsEntitledToUnemploymentSupport(peter));
    }
}

4.2. Lambda와 사용자 지정 답변 의 조합

람다 식을 Mockito의 답변 과 결합하면 동일한 효과를 얻을 수 있습니다 .

예를 들어, Person 의 이름이 "Peter" 인 경우 단일 JobPosition을 포함하는 Stream을 반환하고 그렇지 않으면 빈 Stream을 반환하도록 listJobs() 메서드 에 대한 호출을 시뮬레이트하려면 다음을 생성해야 합니다. Answer 인터페이스를 구현한 클래스(익명 또는 내부) .

다시 말하지만, 람다 식을 사용하면 모든 모의 동작을 인라인으로 작성할 수 있습니다.

public class CustomAnswerWithLambdaUnitTest {
 
    @Before
    public void init() {
        when(jobService.listJobs(any(Person.class))).then((i) ->
          Stream.of(new JobPosition("Teacher"))
          .filter(p -> ((Person) i.getArgument(0)).getName().equals("Peter")));
    }
}

위의 구현에서는 PersonAnswer 내부 클래스가 필요하지 않습니다.

5. 결론

이 기사에서는 새로운 Java 8 및 Mockito 버전 2 기능을 함께 활용하여 보다 깨끗하고 단순하며 짧은 코드를 작성하는 방법을 다루었습니다. 여기에서 본 일부 Java 8 기능에 익숙하지 않은 경우 다음 기사를 확인하십시오.

또한 GitHub 리포지토리 에서 함께 제공되는 코드를 확인하세요 .

res – Junit (guide) (cat=Testing)