스프링 테스트 어노테이션 사용하기에서 스프링 빈 컨테이너가 관리하는 빈(Bean)을 테스트하는 방법에 대해 살펴보았었다. 

    빈과 빈 사이에는 다양한 관계가 있다. 

    이번 시간에는 그 관계를 목(Mock) 객체를 이용하여 끊고, 

    테스트하고자 하는 객체에만 집중하여 테스트하는

    단위 테스트(unit test)에 대해 알아보도록 하겠다.

     

    • 단위 테스트(Unit Test) 

    • 목(Mock) 객체

    빈들 간에는 다양한 관계를 맺고 있는 경우가 많다. 

    하나의 빈을 사용한다는 것은 관계된 빈들도 함께 동작한다는 것을 의미한다. 

    하나의 빈을 테스트할 때 관련된 빈들이 모두 잘 동작하는지 테스트하는 것을

    우리는 통합 테스트(integration test)라 한다. 

    관계된 다른 클래스와는 상관 없이 특정 빈이 가지고 있는 기능만 잘 동작하는지 확인하는 것을 

    우리는 단위 테스트(unit test)라 한다.

     

    다음의 클래스를 추가해보도록 한다.

    package org.edwith.webbe.calculatorcli;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class MyService {
        private final CalculatorService calculatorService;
    
        public MyService(CalculatorService calculatorService) {
            this.calculatorService = calculatorService;
        }
    
        public int execute(int value1, int value2){
            return calculatorService.plus(value1, value2) * 2;
        }
    }

    MyService클래스는 아래의 클래스 다이어그램처럼 CalculatorService를 필드로 가지고 있다.

    해당 필드는 생성자를 통해 초기화를 한다. 

     

    MyService가 가지고 있는 execute메소드는 calculatorService.plus()메소드를 사용한다.

    2개의 값을 받아 plus()메소드를 이용해 더한 후 2를 더한 값을 반환한다.

     

    이렇게 작성한 MyService객체는 다음과 같은 방법으로 테스트를 할 수 있다.

     

    package org.edwith.webbe.calculatorcli;
    
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = {ApplicationConfig.class})
    public class MyServiceTest {
        @Autowired
        MyService myService;
    
        @Test
        public void execute() throws Exception{
            // given
            int value1 = 5;
            int value2 = 10;
    
            // when
            int result = myService.execute(value1, value2);
    
            // then
            Assert.assertEquals(30, result);
        }
    }

     @RunWith(SpringJUnit4ClassRunner.class) 는 내부적으로 스프링 빈 컨테이너를 생성한다고 하였다.

    스프링 빈 컨테이너는 빈들을 찾아 메모리에 올리게 됩니다. 그리고 나서 myService필드에 객체를 주입하게 된다.

     

    테스트 메소드는 myService.execute()를 실행합니다. 5와 10을 더한 값에 2를 곱한 결과를 exeucte()메소드는 반환한다.

     

    assertEquals()메소드로 비교한 결과, 결과는 맞게 나올 거라고 예상할 수 있다. 

    그런데, CalculatorService의 plus()메소드에 버그가 있다면 어떻게 될까? 

    plus()메소드가 더하는 것이 아니라 곱한 결과를 반환한다면 어떻게 될까? 

    아마 내가 생각한 값이 나오지 않을 것이다.

     

    MyService를 테스트하려고 하였지만 CalculatorService의 버그로 인해 에러가 날 수도 있는 것이다. 이런 문제를 해결하려면 어떻게 해야할까? 이런 문제를 해결하는 방법 중 하나는 목(Mock)객체를 이용하는 방법이 있다.

    MyService가 사용하던 CalculatorService를 사용하는 대신, 가짜 객체를 하나 생성하도록 하는 것이다. 내가 원하는 동작을 하는 Mock객체로 CalculatorService를 사용함으로써 MyService의 내용만 테스트를 수행할 수 있다.

     

    이를 위해 pom.xml파일에 다음을 추가한다.

    <!-- test mock을 위한 라이브러리를 추가합니다. -->
            <dependency>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
                <version>1.9.5</version>
                <scope>test</scope>
            </dependency>

    mockito는 오픈소스 목 프레임워크입니다.

    테스트를 위해 가짜 객체를 쉽게 만들어 줄 수 있는 프레임워크라고 생각하면 된다.

     

    package org.edwith.webbe.calculatorcli;
    
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.runners.MockitoJUnitRunner;
    
    import static org.mockito.BDDMockito.given;
    import static org.mockito.Matchers.anyInt;
    import static org.mockito.Mockito.verify;
    
    @RunWith(MockitoJUnitRunner.class)
    public class MyServiceTest {
        @InjectMocks
        MyService myService;
    
        @Mock
    CalculatorService calculatorService;
    
        @Test
        public void execute() throws Exception{
            // given
            int value1 = 5;
            int value2 = 10;
            given(calculatorService.plus(5, 10)).willReturn(15);
            //when
    
            // when
            int result = myService.execute(value1, value2);
    
            // then
            verify(calculatorService).plus(anyInt(), anyInt());
            Assert.assertEquals(30, result);
        }
    }

     

    @RunWith(MockitoJUnitRunner.class)
    

    mockito가 제공하는 Junit 확장 클래스 MockitoJunitRunner를 이용해 테스트 클래스를 실행하도록 한다.

     

    @Mock
    CalculatorService calculatorService;

    @Mock 어노테이션은 calculatorService가 목 객체를 참조하도록 한다.

    즉, 객체를 생성하지 않아도 자동으로 객체가 생성되고 해당 필드가 초기화 된다는 것을 의미한다.

    @InjectMocks
     MyService myService;

    @InjectMocks 어노테이션이 붙은 필드는 목 객체를 사용하는 MyService 객체를 생성하여 초기화하라는 의미를 가진다.

    myService 역시 초기화하지 않아도 자동으로 MyService 객체가 생성되어 초기화 된다.

     

    given(calculatorService.plus(5, 10)).willReturn(15);

    given()은 static메소드

    import문을 보면 org.mockito.BDDMockito 클래스의 static 메소드인것을 알 수 있다. 

    calculatorService는 가짜 객체입니다. 이 가짜 객체가 동작하는 방법을 규정할 수 있는 것이 given()메소드 이다.

    calculatorService.plus(5,10)을 호출하면 plus메소드가 15를 반환하도록 하라는 의미를 가진다. 

    int result = myService.execute(value1, value2);
    

    execute()메소드는 내부적으로 calculatorService의 plus메소드를 호출한다. 

    해당 메소드는 위에서 설정한대로만 동작한다. 

    value1과 value2가 무슨 값이든지 간에 result는 30을 반환할 것이다.

    verify(calculatorService).plus(anyInt(), anyInt());
    

    verify()메소드는 org.mockito.Mockito의 static한 메소드이다. 

    anyInt()메소드는 org.mockito.Matchers의 static한 메소드이다. 

    verify메소드는 파라미터로 들어온 객체의 plus메소드가 호출된 적이 있는지 검증한다.

    좀 더 자세히 살펴보자면 plus(anyInt(), anyInt())는 어떤 정수든지 2개를 파라미터로 넣어서

    plus()메소드를 호출했는지를 검증한다. 

    myService.execute()메소드 내부적으로 plus 메소드를 호출했다면 해당 메소드는 검증을 성공한 것이다.

    만약, execute()메소드에서 plus(anyInt(), anyInt())를 호출하지 않았다면 오류가 발생하게 된다.

     

    public int execute(int value1, int value2){
            int value = 0;
            // value = calculatorService.plus(value1, value2); // plus메소드가 호출되지 않도록 주석문 처리하였다.
            return  value * 2;
        }
        

    execute() 메소드를 위와 같이 수정한 후 테스트를 실행해보겠다

    Wanted but not invoked:
    calculatorService.plus(<any>, <any>);
    -> at org.edwith.webbe.calculatorcli.MyServiceTest.execute(MyServiceTest.java:33)
    Actually, there were zero interactions with this mock.
    
    Wanted but not invoked:
    calculatorService.plus(<any>, <any>);
    -> at org.edwith.webbe.calculatorcli.MyServiceTest.execute(MyServiceTest.java:33)
    Actually, there were zero interactions with this mock.
    
    	at org.edwith.webbe.calculatorcli.MyServiceTest.execute(MyServiceTest.java:33)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    	at org.mockito.internal.runners.JUnit45AndHigherRunnerImpl.run(JUnit45AndHigherRunnerImpl.java:37)
    	at org.mockito.runners.MockitoJUnitRunner.run(MockitoJUnitRunner.java:62)
    	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

    위와 같이 해당 메소드가 호출한 적 없다는 Exception이 발생하면 테스트는 실패한 것이다.

     

    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기