본문 바로가기

Spring/단위테스트

1) Spring boot 단위테스트 1부 - @Service 영역테스트

반응형

단위테스트에 대해 알아보자.

Spring MVC 는 크게 @Controller , @Service , @Repository 로 나눤다.

 

Springboot는 org.springframework.boot:spring-boot-starter-test 를 지원하면서 간단히 테스트에 필요한 

라이브러리를 지원한다.

 

build.gradle파일에 test를 추가해보자.

plugins {
	id 'org.springframework.boot' version '2.4.3'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

group = 'com.devracoon.jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
	implementation 'com.querydsl:querydsl-jpa'
	implementation 'com.querydsl:querydsl-apt'
	implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.5'
	
	compileOnly 'com.h2database:h2'
	compileOnly 'org.projectlombok:lombok:1.18.6'
    annotationProcessor 'org.projectlombok:lombok:1.18.6'
    
    testImplementation('org.springframework.boot:spring-boot-starter-test')
}


def querydslSrcDir = "$buildDir/generated/querydsl" 

querydsl { 
    library = "com.querydsl:querydsl-apt" 
    jpa = true 
    querydslSourcesDir = querydslSrcDir 
} 
compileQuerydsl{ 
    options.annotationProcessorPath = configurations.querydsl 
} 
configurations { 
    querydsl.extendsFrom compileClasspath 
}

sourceSets { 
    main { 
        java { 
            srcDirs = ['src/main/java', querydslSrcDir] 
        } 
    } 
}



test {
	useJUnitPlatform()
}
testImplementation('org.springframework.boot:spring-boot-starter-test')

이부분을 추가하게 @Service 영역에 대한 테스트는 가능하게 된다.

 

테스트를 위해 간단한 Repository 와 Service 를 하나 만들었다.

package com.devracoon.jpa.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.devracoon.jpa.entity.Product;
import com.devracoon.jpa.repository.ProductRepository;

@Service
public class JunitTestServiceImpl implements JunitTestService {
    @Autowired
    private ProductRepository productRepo; 

    public Product findTest(String testParam) {
        Product product =  productRepo.findById(testParam).orElse(null);
        return product;
    }
}

 

간단한 Product를 id로 조회하는 소스이다. 이제 테스트 소스를 만들어 보자.

package com.devracoon.jpa.service;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.*;

import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.devracoon.jpa.entity.Product;
import com.devracoon.jpa.repository.ProductOrderRepository;
import com.devracoon.jpa.repository.ProductRepository;

@ExtendWith(MockitoExtension.class)
class SampleServiceImplTest {
    
    @Mock
    private ProductRepository productRepo;
    
    @Mock
    private ProductOrderRepository productOrderRepo; 
    
    @InjectMocks
    private JunitTestServiceImpl testService;
    
    @Test
    void testFindProductByQuery() {
        //given
        final String productId = "productId";
        given(productRepo.findById(any())).willReturn(Optional.of(new Product("product-test")));
        
        //when
        Product product = testService.findTest(productId);
        
        //then
        assertNotNull(product);
        
    }
}

 

각 어노테이션들에 대해 알아보자.

버전 Spring MVC 테스트 단위 테스트(Mockito) Spring 통합테스트
Spring boot 2.2 이전 @RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest
Spring boot 2.2 이후 @WebMvcTest @ExtendWith(MockitoExtension.class) @SpringBootTest

 

@ExtendsWith : 지금처럼 Service 영역에 대한 단위테스트를 위해서  사용하면 된다.

@Mock :  Mock이란 "실제 객체를 만들어 사용하기에 시간, 비용 등의 Cost가 높거나 혹은 객체 서로간의 의존성이

             강해 구현하기 힘들 경우 가짜 객체를 만들어 사용하는 방법이다."  라고 정의 되어 있다.

             정의 에서 말하듯이 @Mock은 테스트를 필요한 가짜 객체이다.

 

@Test : 테스트 함수를 지정

 

이제 각 row별로 확인해 보자.

given(productRepo.findById(any())).willReturn(Optional.of(new Product("product-test")));

위 소스는 @Mock으로 설정된 가짜 객체인 productRepo의 findById 함수가 호출되면 willReturn이 실행된다.

즉 정상적이라면 해당함수는 DB에서 데이터를 조회를 해야 하지만 여기서는 Service의 함수를 테스트하는 것이지

Repository를 테스트하는것이 아니기 때문에 productRepo를 Mock Object로 만들었고 그에 따라 

해당 함수가 리턴하는 결과값도 우리가 의도하는 값으로 Return하게 만드는것이다.

 

Product product = testService.findTest(productId);

그리고 실제 findTest 함수가 실행되면 해당 함수가 실행되고 해당 함수 안에 productRepo.findById가 실행되면 

위에 given으로 만들어놓은 결과값이 리턴될것이고 그외에 service 의 함수에 있는 로직은 진행되고 

결과값이 리턴됩니다.

 

그리고 해당 결과에 대한 의도된 값이 맞는지 등에 대한 결과 체크를 한다.

 

이렇게 Repository가 정상 작동한다는 가정을 하고 Service의 대한 테스트만 하고자 할때 사용한다.

 

만약 Service 및 Repository를 DB 연결까지 전체 테스트를 하고자 한다면 아래와 같이 

package com.devracoon.jpa.service;

import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.devracoon.jpa.entity.Product;

//@ExtendWith(MockitoExtension.class)
//@ExtendWith(SpringExtension.class)
@SpringBootTest
class SampleServiceImplTest {
    
//    @Mock
//    private ProductRepository productRepo;
//    
////    @Mock
//    private ProductOrderRepository productOrderRepo; 
    
//    @InjectMocks
    @Autowired
    private JunitTestServiceImpl testService;
    
    @Test
    void testFindProductByQuery() {
        //given
        final String productId = "f0cbb868-abb7-4125-a78b-b6bf7247cb38";
//        given(productRepo.findById(any())).willReturn(Optional.of(new Product("product-test")));

        //when
        Product product = testService.findTest(productId);
        
        //then
        assertNotNull(product);
        
    }
}

@SpringBootTest 를 붙여주고 Mock Object가 아닌 SpringContext Object를 Autowired 해서 사용하면 된다.

 

단점은 실제 SpringContext를 전부 Load를 하기 때문에 test실행에 시간이 걸린다는 단점이 생긴다. 

테스트 코딩을 하면서는 사실 좀 답답한면이 생기게되서 잘 사용하지 않게 되는것 같다.