본문 바로가기

TEST CODE

[JUnit] Test code 작성시 DI(Dependencies Inject) 를 적용하는 방법

 

 

Spring boot 에서 테스트 코드를 작성할 때, 고민되는 것 중 하나가 의존성 문제 해결입니다.
한 Service 가 다른 Service 를 의존하여 실행되거나, Controller 가 Service 를 의존하거나, Mybatis 를 사용하는 경우 Service 에서 Mapper 를 의존하는 경우가 대부분입니다.

이럴 때, 어떤 의존성을 가지냐와 서로가 어떻게 실행될 것인지에 대해서 의존성을 달리 주입할 필요가 있습니다.

DIIOC 에 관해서 궁금하신분은 아래 velog를 참고 부탁드립니다. 직관적으로 설명이 잘되어있습니다.
IoC? DIP? IoC Container? DI? DI Framework? 도대체 그게 뭔데?



 

Mapper

Spring boot 에서 RDBS 를 사용하여 MVC 구조에서 개발을 하는 경우에는 대부분 Mapper 인터페이스를 이용해서 Mybatis 프레임워크를 사용하는게 대부분일 것입니다. 이러한 구조에서 Service Layer 의 unit test 를 진행할 때 mapper 의 의존성을 주입이 필수적입니다.

이러한 mapper 의 의존성을 해결하기 위해서 경우의 수가 2가지 존재합니다.

첫번째로는, mapper 에 존재하는 mybatis 자체를 테스트하는 경우입니다. 현재 mybatis 공식 홈페이지에서 다음과 같이 가이드라인을 제시하고 있습니다.

mybatis-spring-boot-test-autoconfigure

또한, 이러한 mybatis test 에 대해서 기여하시고, 샘플코드도 제공하시는 글도 존재합니다.

머루의개발블로그:스프링캠프와 Spring Boot Mybatis Test

우선, Gradle 의 경우 build.gradle 에 다음과 같이 추가해줍니다.

dependencies {
    testCompile("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.1.1")
}

application.yml 에 다음과 같은 내용을 추가해줍니다.

mybatis.type-aliases-package=com.example.test
mybatis.mapper-locations=sample.xml
spring.datasource.schema=schema.sql

그리고, 다음과 같이 테스트 코드를 작성해줍니다.

@RunWith(SpringRunner.class)
@MybatisTest
public class LoginTest {

  @Autowired
  private LoginMapper loginMapper;

  @Autowired
  private SqlSession sqlSession;

  @Before
  public void setup() {
    Map<String, Object> User = new HashMap<String, Object>();
    User.put("email", "test@test.com");
    User.put("name", "kim");
    User.put("password", "1234567");
    sqlSession.insert("saveUser", User);
  }

  @Test
  public void sqlSessionTest() {
    Sample findUser = sqlSession.selectOne("findUser", 1L);
    assertThat(findUser.getEmail()).isNotNull().isEqualTo(1L);
  }

  @Test
  public void sampleMapperTest() {
    User user = sampleMapper.findByname("kim");
    assertThat(user.getEmail()).isEqualTo("test@test.com");
  }
}

이처럼 @Mybatis annotation 을 사용하여 의존성 주입 후 실질적으로 데이터가 저장되고 조회되는지를 테스트할 수 있습니다.

 

Mock

두번째는 Mokito 를 활용하여 의존성을 주입하는 방법입니다. 이전 포스트(Test 와 TDD) 에서 Mock 에 대한 설명과 사용법에 대해서 설명한 내용이 있습니다. 궁금하신 분들은 참고 부탁드립니다.

Mockito 를 사용하게되면 Mock 이라는 객체로 의존성을 주입해줍니다. 하지만, 말 그대로 '가짜' 객체를 생성하는 것이기 때문에 실제로 Mock 을 주입한 객체는 '가정' 만 가능하며 실제 로직을 흐르게 할 수 없습니다.

다음은 패스워드가 틀린 경우 DB 에 틀린 횟수를 1회 추가해주는 로직을 테스트하는 로직입니다.

@RunWith(MockitoJUnitRunner.class)
public class CMSServiceTest {
    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;


    @Test
    public void checkPasswordFailCount_패스워드가_틀린_경우() throws Exception {
        //given
          User user = new User().builder()
                                    .userId("id")
                                    .passwordFailCount(5);
          
        //when
        when(userMapper.updatePasswordCount(any())).thenReturn(6);

        //then
        assertThat(userService.checkPasswordFailCount(checkPassword)).isEqualTo(6);
    }
}

UserMapper 는 Mociking 의 대상이고, 이러한 Mock 객체를 주입당하는 Service 가 UserService 입니다.
'가짜' 객체를 원하는 클래스에 @Mock 을 선언해주고, 이러한 Mock 객체를 '주입'하고자 하는 클래스에 @InjectMocks 를 걸어주면 됩니다.

이처럼 6회로 update 되었다는 가정을하여 저장을 해준 후 다시한번 검사할 때 6회로 검사되어 테스트가 성공되도록 진행되었습니다.

Mock 은 테스트에서 유용한 요소이지만 가능한 적게 사용하는 것이 테스트의 정확성이 증가됩니다.

Mock 사용 시 주의해야할 점은, UserMapper 에 Mock 을 거는 순간 UserMapper 이라는 Interface 자체에 Mock 이 걸려 Mock 객체가 생성되는 것이기 때문에 내장되어있는 메서드는 모두 Mock 이 걸려 로직을 사용할 수 없게 됩니다.

 

Spy

세번째 의존성을 주입하는 방법은 Spy 입니다. 이 annotation 은 Mock 과 혼동하기가 쉬운데, Mock 은 @Mock 을 선언한 클래스가 전부 Mocking 되어 모든 기능을 사용할 수 없게됩니다. 따라서 실질적으로 로직이 흘러야 하는 부분이 존재하게 되는데, 이런 경우 @Spy annotation 을 사용하면 됩니다.

Mock 과 Spy 의 차이점은 Mock 을 사용하면 실질적인 instance 가 아닌 가짜 객체를 생성하는 반면에 Spy실질질적으로 존재하는 instance 를 wraping 한 객체를 생성합니다. 실제 객체와 같이 동작하며 실제 객체와 다른점은 단지 interactions 를 추적 가능하다는 점입니다.

LoginService.java

public 특정_서비스_처리를_위한_메서드() {
    ...
    ...

    if (isNotSuccessCode()) {
        responseCodeService.logginTcpResponse(codeInfo);
    }
    
    return SUCCESS;
}

위에서 작성된 코드는 특정 기능을 처리하는 메서드인데, 만약 응답 코드가 성공이 아니면 오류코를 별도의 서비스에서 처리하도록 작업이 되어있습니다.

여기서, 만약 LoginService 에 Mock 을 선언하여 테스트를 작업한다면 테스트 코드가 오류가 발생할 것입니다. 그 이유는, LoginService 는 '가짜' 객체로 초기화가 되었지만, responseCodeService 는 별도의 초기화 작업이 되지 않았기 때문에 인스턴스가 생성되지 않은 관계로 responseCodeService 가 NullPointException 이 발생하는 것입니다.

따라서, 이를 해결하기 위해서는 LoginService 는 Mock 객체로 생성이 되지만, responseCodeService 는 정상로직을 실행할 수 있도록 객체를 주입해줘야 합니다.

이런 기능을 제공하는 것이 @Spy 입니다.

@RunWith(MockitoJUnitRunner.class)
public class CMSServiceTest {
    @Mock
    private LoginMapper loginMapper;

    @Spy
    @InjectMocks
    private ResponseCodeService responseCodeService;
    
    @InjectMocks
    private LoginService loginService;

위의 예시를 보면 기존에 Mock 테스트하는 것처럼 추가해준 후, 실제로 로직이 실행되기 위한 객체 선언부 위에 @Spy 를 선언해주면 실제 인스턴스가 주입되면서 로직이 실행되게 됩니다.

 

Autowired

Spring 으로 프로젝트를 진행하면 가장 쉽게 객체에 의존성을 주입하는 방법 중 하나가 Autowired anotation 입니다.

Autowired의존 객체 자동 주입(Automatic Dependency Injection) 의 기능을 가지고 있으며 스프링 설정파일에서 혹은 태그로 의존 객체 대상을 명시하지 않아도 스프링 컨테이너가 자동적으로 의존 대상 객체를 찾아 해당 객체에 필요한 의존성을 주입하는 것을 말합니다.

@Autowired 는 주입하려고 하는 객체의 타입이 일치하는지를 찾고 객체를 자동으로 주입합니다. 만약, 타입이 존재하지 않는다면 @Autowired 에 위치한 속성명이 일치하는 bean 을 컨테이너에서 찾습니다. 그리고 이름이 없을 경우 @Qualifiter Annotation 의 유무를 찾아 그 Annotation 이 붙은 속성에 의존성을 주입합니다.

이러한 Autowired 가 선언된 Service 를 실제 로직을 실행해 테스트하기 위해서는 테스트 서비스 내에도 동일하게 Autowired 로 객체를 주입해줘야 합니다.

이를 위해서는 다음과 같은 선언이 필요합니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

     @Autowired
     private UserService userService;
}

위에 class 선언 부 위를 보시면 @Runwith@SpringBootTest 를 볼 수 있습니다.

@RunWith 란, Spring 프레임워크의 실행 방법을 확장할 때 사용하는 어노테이션입니다. SpringRunner 라는 class 를 지정해주면, JUnit 프레임워크가 내장된 Runner 를 실행할 때 SpringRunner.class 라는 확장된 클래스를 실행하라고 지시하는 것입니다.

@SpringBootTest 는 Spring boot 어플리케이션 테스트 시 테스트에 필요한 거의 모든 의존성을 제공해줍니다. 제공해주는 목록은 다음과 같습니다.

 - JUnit: The de-facto standard for unit testing Java applications.  
 - Spring Test & Spring Boot Test: Utilities and integration test support for Spring Boot applications.  
 - AssertJ: A fluent assertion library.  
 - Hamcrest: A library of matcher objects (also known as constraints or predicates).
 - Mockito: A Java mocking framework.  
 - JSONassert: An assertion library for JSON.  
 - JsonPath: XPath for JSON.  

이러한 Autowired 를 실행하기 위해서 위와 같은 목록들이 실행되어야 하기 때문에 Spring boot 가 빌드되면서 테스트 실행속도가 현저히 느려지게 됩니다. 따라서, 테스트 실행 횟수가 줄어들게 됩니다.
테스트는 로직이 수정될 때마다 주기적으로 실행을 해줘야하는데 실행 속도가 느려지게되면 빈도수가 줄어들게 되어 테스트의 활용도가 떨어지게 됩니다.
따라서 가능한 서비스간에는 의존성 주입을 적게 해주는 것이 좋습니다.

 

 

 

총 4가지 방법의 테스트 시 의존성 주입방법에 대해 설명하였습니다.

이 4가지는 각각 상황에 따라 사용 용도 따라 사용할 수 있습니다. 따라서, 실제 서비스 로직을 작성할 때 이러한 의존성 주입에 대해 고려하여 작성하는 것도 필요합니다.
만약, TDD 로 구현을 한다면 테스트를 먼저 작성하여 서비스 로직을 구현하기 때문에 이러한 의존성을 먼저 고려하여 설계할 수 있기 때문에 테스트 작성이 훨씬 수월해 질 수 있습니다.