새소식

Java

[Spring] Spock으로 테스트 코드를!

  • -
728x90
Spock으로 테스트코드를 작성하며 경험한 것들을 기록한다.📖

 

 

⛅️ Intro

TDD 가 중요 역량이 되는만큼 테스트 코드 작성은 매우 중요하다. 그렇기에 테스트 코드를 생활하는 습관을 들이는 도중, 주변에서 "JUnit 말고 Spock 도 사용해보세요 !! "라는 말과 권유는 많이 들었지만 할게 너무 나도 많아서 미뤄왔다. 그런데 막상 테스트를 작성하다 보니 생각보다 간단했고 아직 Spock을 잘 모르시는 분과 함께 경험을 나누면 좋겠다고 생각해서 이 글을 정리한다. 이제 막 시작하는 단계이다 보니 부족한 점이 많지만.. 

 

Spring Boot 에서 Spock 로 테스트 

Spring Boot 에서 Spock 를 사용하는 방법에 대해 설명하기 전에 Spock가 뭔지, 사용법은 무엇인지에 대해 알아보자.

 

Spock 란 ?

Spock은 Java와 Groovy 기반의 애플리케이션을 위한 테스팅 및 명세 프레임워크다. Groovy 언어의 간결함 덕분에 코드의 가독성과 유지보수성이 향상된다. Spring 프레임워크와 함께 Spock을 사용하면 Spring 애플리케이션의 구성 요소들을 효과적으로 테스트할 수 있다.

 

Spock의 주요 특징

  • 강력한 테스트 작성: Spock은 BDD(Behavior-Driven Development) 방식을 채택하여 given, when, then 등의 블록을 사용해 테스트 케이스를 명세하고 구현할 수 있게 한다. 이를 통해 테스트의 목적과 조건, 결과를 명확하게 표현할 수 있다.
  • 통합 테스트 지원: Spock은 Spring 애플리케이션의 통합 테스트를 지원한다. @SpringBootTest와 같은 Spring 테스트 애너테이션과 함께 사용할 수 있으며, Spring 애플리케이션 컨텍스트를 로드하여 빈 주입 등의 기능을 사용할 수 있다.
  • Mocking 및 스텁 기능: Spock은 Groovy 기반의 동적 언어 특성을 활용하여, 간단하고 강력한 mocking과 스텁(stubbing) 기능을 제공한다. 이를 통해 의존성을 가진 컴포넌트를 쉽게 가상화하여 테스트할 수 있다.
  • 데이터 주도 테스트: Spock은 데이터 주도 테스트를 지원하여, 다양한 입력 값에 대해 동일한 테스트 로직을 반복 실행할 수 있게 해준다. 이를 통해 코드의 재사용성을 높이고, 다양한 시나리오를 효율적으로 테스트할 수 있다.

Spock 기본 사용법

setup & cleanup

- setup

JUnit의 @Before 과 같은 기능

모든 테스트 메소드가 각각 실행되기 전에 수행된다.

 

- cleanup

JUnit의 @After 과 같은 기능

모든 테스트 메소드가 각각 실행된 후에 수행된다.

 

이외에도 setupSpec, cleanupSpec등이 더 있는데, 이런 메소드를 보고 Spock에선 fixture 메소드 라고 한다. 관련해선 위 링크를 통해 공식 문서를 보시면 바로 이해할 수 있다. JUnit 코드와 다른 점이 한가지 더 있다면 SpringRunner.class가 빠진 것이다. 

@RunWith(SpringRunner.class)는 Spock 테스트에서 사용하지 않는다.

block

Spock에서는 given, when, then과 같은 코드 블록을 block이라 부른다. block은 테스트 메소드 (feature method) 내 최소한 하나는 있어야 한다. JUnit에서는 있어도 그만 없어도 그만이었는데 Spock에서는 필수다!

 

  • given (또는 setup) : JUnit의 //Given처럼 테스트에 필요한 환경을 설정하는 작업. 항상 다른 블록 보다 상위에 위치해야함.
  • when : 테스트코드를 실행
  • then : 테스트코드 결과 검증, 예외 및 조건에 대한 결과를 확인할 수 있고 작성한 코드 한줄이 assert 문
  • expect : 테스트할 코드 실행 및 검증 (when + then)

의존성 추가

먼저 Spock을 사용하기 위해서 아래 두 의존성을 추가해야 한다. (Gradle 기준) 또한 test 디렉토리에 groovy 라는 디렉토리를 생성해 주어야 한다.

// spock
testImplementation('org.spockframework:spock-core:2.1-groovy-3.0')
testImplementation('org.spockframework:spock-spring:2.1-groovy-3.0')

 

짜잔

 

Spock 를 추가하면 위와 같은 사진을 볼 수 있다. Spock는 groovy 를 사용한다. groovy 는 아직 나에게 벅찬 내용이기에 다음 기회에 ..

 

기능 테스트를 위한 테스트 코드를 작성해보자

직접 프로젝트를 진행하며 테스트 한 것들을 통해 이어 설명해보겠다.

 

@Slf4j // log
@Service
@RequiredArgsConstructor
public class KakaoAddressSearchService {

    private final RestTemplate restTemplate;
    private final KakaoUriBuilderService kakaoUriBuilderService;

    @Value("${kakao.rest.api.key}")
    private String kakaoRestApiKey;

    public KakaoApiResponseDto requestAddressSearch(String address) {
        if(ObjectUtils.isEmpty(address)) return null;

        URI uri = kakaoUriBuilderService.buildUriByAddressSearch(address);

        // SET Header
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.AUTHORIZATION, "KakaoAK " + kakaoRestApiKey);
        HttpEntity httpEntity = new HttpEntity<>(headers);

        // kakao api 호출, Body 만 return
        return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, KakaoApiResponseDto.class).getBody();
    }
}

..
..

@Service
public class KakaoUriBuilderService {

    private static final String KAKAO_LOCAL_SEARCH_ADDRESS_URL = "https://dapi.kakao.com/v2/local/search/address.json";

    public URI buildUriByAddressSearch(String address) {
        UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(KAKAO_LOCAL_SEARCH_ADDRESS_URL);
        uriBuilder.queryParam("query", address);

        URI uri = uriBuilder.build().encode().toUri();
        log.info("[KakaoUriBuilderService buildUriByAddressSearch] address : {}, uri : {}" ,address, uri);

        return uri;
    }
}

 

위의 코드는 실제 카카오 주소 검색 API 를 사용하여 데이터를 받아올 때 사용한 로직이다. 이 기능에 대한 테스트 코드를 작성했다! 여기서 AbstractIntegrationContainerBaseTest 를 확장했는데 이는 testContainer를 사용하여 테스트 DB를 구성하고 그에 대한 통합 테스트 환경을 구성했는데 이는 설명할 내용이 너무 많아.. 따로 글을 작성해서 설명하겠다. 

 

class KakaoAddressSearchServiceTest extends AbstractIntegrationContainerBaseTest {

    @Autowired
    private KakaoAddressSearchService kakaoAddressSearchService

    def "address 파라미터 값이 null이면, requestAddressSearch 메소드는 null 을 반환한다."() {
        given:
        String address = null;

        when:
        def result = kakaoAddressSearchService.requestAddressSearch(address)

        then:
        result == null
    }

    def "주소 값이 valid하면, requestAddressSearch 메소드는 정상적으로 document를 반환한다."() {
        given:
        String address = "서울 성북구 종암로 10길";

        when:
        def result = kakaoAddressSearchService.requestAddressSearch(address)

        then:
        result.documentList.size() > 0
        result.metaDto.totalCount > 0
        result.documentList.get(0).addressName != null
    }
}

 

위의 코드를 보면 JUnit과 굉장히 비슷하면서도 더 간결하다! 또한 @Autowired 어노테이션을 사용하여 Spring 컨테이너로부터 KakaoAddressSearchService의 인스턴스를 자동 주입받는 형식까지 모두 동일하다. 그럼 테스트를 자세히 살펴보자!

 

첫 번째 테스트 케이스는 주소 파라미터 값이 null일 때의 동작을 테스트하는 코드다. 이는 requestAddressSearch 메서드에서ObjectUtils.isEmpty(address) 에서 null 이 반환됨을 확인한다. 그렇기에 차례대로 

 

given: // given 블록에서는 테스트의 전제 조건을 설정.
String address = null;

when: // when 블록에서는 테스트를 수행할 행동을 정의.
def result = kakaoAddressSearchService.requestAddressSearch(address)

then: // then 블록에서는 예상되는 결과를 검증.
result == null

 

이렇게 코드를 작성하고 성공하길 기도하며 테스트를 돌려보면 .. 

 

이게 왜 이래 ?

 

처참하게 실패했다 😞

왜일까 ? 실패의 원인을 파헤칠 필요도 없을만큼 짧은 코드인데..

그 이유는 테스트 'KakaoAddressSearchService' 서비스가 환경 변수의 값을 사용하고 있기 때문이다.

@Value("${kakao.rest.api.key}")
private String kakaoRestApiKey;
java.lang.IllegalStateException: Failed to load ApplicationContext

	at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:132)
	at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:124)
	at org.springframework.test.context.web.ServletTestExecutionListener.setUpRequestContextIfNecessary(ServletTestExecutionListener.java:190)
	at org.springframework.test.context.web.ServletTestExecutionListener.prepareTestInstance(ServletTestExecutionListener.java:132)
	..
    ..
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'kakaoAddressSearchService': Injection of autowired dependencies failed; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'KAKAO_REST_API_KEY' in value "${KAKAO_REST_API_KEY}"
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:405)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1431)
	... 50 more
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'KAKAO_REST_API_KEY' in value "${KAKAO_REST_API_KEY}"

 

위의 에러 메세지에서 확인할 수 있듯, 테스트 코드에서 api key 값을 찾을 수가 없어서 에러가 발생했다. 이를 통해 테스트 코드를 작성할 때 독립적인 환경에서 테스트를 할 수 있게 환경 변수와 같은 설정을 테스트 코드 별로 설정해줘야함을 배웠다. 다시 환경 변수를 설정해주고 테스트를 돌리면 ..

 

속도가 느린 것은 테스트 컨테이너 때문 ..

깔끔하게 성공할 수 있다 ⭐️

 

오늘은 Spock Framework 를 통한 테스트 코드를 작성해봤다. 충분한 내용을 전달하는 글은 아니였지만 복잡한 테스트 코드를 작성할 때를 위한 발돋움이라 생각한다. 또한 더 많이 학습하고 정리해둬야 할 필요성을 느끼는 보람찬 경험으로 남겨둔다 🌱

 

[참고]

https://jojoldu.tistory.com/229

https://techblog.woowahan.com/2560/

728x90
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.