TOSS SLASH21 — 테스트커버리지 100% 요약/DEMO
나도 커버리지 100% 를 만들어보자.
jacoco +Spring REST docs + restdocs-api-spec + SwaggerUIBundle
영상의 요약과 후기는 주관적으로 작성하였습니다.
TL;DR — 영상
- 테스트 커버리지 100%는 생각보다 할 만하다.
- 테스트 커버리지가 낮으면 빌드가 실패하게 하자.
- 테스트는 빨라야한다.
테스트 커버리지의 범위 100%성과
2천라인의 테스트코드, 그리고 50%의 테스트 커버리지
-> 2개월동안 6천라인의 테스트코드를 작성. 테스트커버리지를 100% 달성
테스트 커버리지를 계속 유지하는 방법
차츰 테스트 커버리지를 높여가며 기준점 이하일 경우 배포가 불가능하도록 제한
높은 테스트 커버리지의 이점
- 테스트 100% 보장 -> 부담없는 배포가 가능해짐
- 거침없이 리팩토링을 할 수 있게 됨
- 불필요한 코드 제거 -> 테스트 필요성이 없어지는 코드를 바로 제거
- 코드에 대한 이해도 향상
- 테스트케이스의 패턴이 생겨 빠르고 쉽게 작성할 수 있게 됨
Gradle Jacoco의 jacocoTestCoverageVerification으로 테스트수치를 체크한 후 배포되도록 할 수 있다.
느려지는 테스트를 개선하는 방법
1) Spring Application Context 로딩을 제거
-> Staic Mocking을 이용해 Spring을 Mocking한 후 실제 스프링 기동 없이 SpringApplication.run()이 호출되는지만 확인
2) 프로파일링(IntelliJ Async-profiler로 측정)
1분이 초과되는 테스트 -> 40초미만으로 개선
- SLF4J 초기화 -> 필요없는 로깅 제거
- Jackson ObjectMapper() -> Gson으로 교체
- Handlebars 컴파일 -> Handlebars 캐시 적용
- Byte Buddy 초기화 -> 테스트에서 사용 중단
- 코틀린 리플렉션 모듈 초기화 -> isSubclassOf 함수 호출 제거
- MockK -> 필수적이지 않으면 제거
- 순차적 테스트 실행 -> 클래스단위로 테스트 병렬 실행
3) 장비교체
40초 미만의 테스트가 6초대로 개선
4) 어려운 테스트
- 대부분의 테스트는 모킹으로 해결 가능
- 코틀린이 생성해낸 바이트코드 테스트가 어려움
-> 문법으로 우회하거나, 특정파일을 제외해 해결
왜 100%여야 하는가
99%는 운 좋게 넘어갈 수도, 억울하게 실패할 수도 있다.
100%를 유지하는 것이 오히려 더 간단하다.
테스트케이스를 보완하는 방법
- Mutation Testing -> 오랜시간이 걸리므로 검토 필요
- 테스트코드 스팩문서 만들기
- 컴포넌트간 값이 올바르게 전달받지 못했을 때
Consumer Driven Contracts 기법 고민.
Spring Cloud Contract vs Pact
Pact를 사용하기로 함
한번 영상에 나온대로 직접 개선해보기로 했습니다.
TL;DR — DEMO
- ATDD로 기본 테스트코드 작성
- jacoco로 테스트 커버리지를 통한 빌드 제어
- Spring REST docs + restdocs-api-spec +Swagger UI 문서화
Step1
- 회원가입기능과 회원가입한 사용자의 시퀀스값으로 회원정보를 조회하는 단순한 API를 만들었습니다.
Step2
- jacoco를 적용해보았습니다.
// 추가된 항목만 작성
// build.gradleplugins {
id 'jacoco'
}jacoco {
toolVersion = "0.8.5" //2021.04.30 기준 최신
}
jacocoTestReport {
reports {
html.enabled true
csv.enabled true
xml.enabled false
}
finalizedBy 'jacocoTestCoverageVerification'
}jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true
element = 'CLASS'
// includes = []
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.84
}
excludes = []
}
}
}test {
finalizedBy 'jacocoTestReport'
}
- 실행
./gradlw test
- 테스트 결과
1) 최초빌드 75%. 29sec.
2) 테스트 커버리지 보완 100%. 28sec.
- Jacoco Test의 Build를 Success시키기 위해서는 누락된 테스트케이스들을 추가로 개발해서 코드의 안정성을 높일 필요가 있었습니다.
- IntelliJ의 Analyze > Inspect Code로 간단한 취약점들을 확인할 수 있었습니다.
- Async-Profiler도 실행시켜 보았지만 현재 개발하거나 적용한라이브러리, 기능들이 거의 없어서 뚜렷하게 수정할만한 부분은 찾지 못했었습니다.
- 테스트코드가 많아진만큼 시간이 더 오래 걸렸지만(37초), 클래스기준 병렬로 테스트하도록 변경해 빌드시간을 더 줄였습니다.
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.classes.default = concurrent
3) 장비교체
MacBook Pro (13-inch, 2017, Four Thunderbolt 3 Ports)
3.1 GHz 듀얼 코어 Intel Core i5
16GB 2133 MHz LPDDR3
-> 28secWindow x64
3.2 GHz Intel Core i5 6500
16GB
-> 39sec
?? 별 소득이 없다.
Step3
테스트로 스펙 문서 만들기
임시로 README에 API Spec를 적었지만 자동화로 Spec문서를 만들어야 지속적으로 변경을 관리하고 최신화를 해줄 수 있습니다. 영상에서는 Cucumber -> JUnit5의 TestExecutionListener를 커스터마이징해 스펙문서를 만들었지만, 저는 조금 다른 스팩으로 문서화를 해보기로 했습니다.
Document Spec 히스토리
- Swagger와 RestDocs
많은 개발자들이 Swagger와 Spring REST Docs를 비교합니다.
Swagger의 장점은 어노테이션을 기반으로 문서를 만들기 때문에 API Spec 작성이 쉽습니다. 하지만 테스트와 별개된 문서화가 필요하기 때문에 관리가 어렵습니다.
Spring REST Docs의 장점은 성공한 테스트를 기반으로 문서화가 만들어지기 때문에 테스트가 강제화됩니다. 그리고 별도의 코드도 필요하지 않습니다.
단, 테스트코드가 복잡해 질 수도 있다는 단점이 있습니다.
- Spring REST docs + restdocs-api-spec +Swagger UI
1)검증된 테스트 (Swagger < Spring REST docs)로
2)보기좋은 문서( Swagger UI> Spring REST docs AsciiDoc )를
3)쉽게(restdocs-api-spec) 만들자.
- Spring REST Docs 추가
// build.gradle
plugins {
id 'org.asciidoctor.convert' version '1.5.8'
}ext {
set('snippetsDir', file("build/generated-snippets"))
}dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
}test {
outputs.dir snippetsDir
}asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
Spring REST Docs가 우선 기본 문서화 파일들인 adoc을 만들어주는지 확인해야합니다.
제공할 기능들을 테스트하는 인수테스트에서 공통으로 사용하기 위해 만들었던 AcceptanceTest 클래스에 Rest Docs를 적용하기 위해 위의 항목들을 추가했습니다.
그리고 document생성을 위해 기존에 ATDD로 작성했던 코드를 일부 수정합니다.
마지막으로 gradle test를 구동해주면
일단 지정한 build/generated-snippets 경로에결과물이 나왔다는 것을 확인할 수 있습니다.
여기서 끝내면 REST Docs만 사용하고 끝입니다.
하지만 끝내선 안됩니다.
- restdocs-api-spec 추가
// build.gradle
plugins {
id 'com.epages.restdocs-api-spec' version '0.11.3'
}dependencies {
testCompile('com.epages:restdocs-api-spec-restassured:0.11.3')
}openapi3 {
server = 'https://localhost:8080'
title = 'TEST-COVERAGE-PERFECT-DEMO'
description = 'we need coverage perfect 100%'
tagDescriptionsPropertiesFile = 'src/docs/tag-descriptions.yaml'
version = '0.1.0'
format = 'yaml' // yaml or json
}
그리고 Rest Docs의 document를 Wrapping해서 yaml or json으로 만들어 줄 RestAssuredRestDocumentationWrapper로 바꿔줍니다.
그리고 ./gradlew openapi3 를 실행시켜주면
위와 같이 yaml파일이 만들어진 것을 확인할 수 있습니다.
4–3) Swagger UI 적용
Swagger를 Spring에 설치할 수도 있지만, 여기서는 간단하게 HTML파일로 동작하게 구현했습니다.
static 경로 아래에 swagger.html 파일을 만들고 아래와 같이 관련된 스크립트를 Swagger UI Bundle 식에 맞게 작성해줍니다.
그리고 static경로에서 restdocs-spec이 생성해준 spec파일들을 읽어야하는데,
임의로 output directory를 static 폴더 아래로 지정합니다.
4–4) openapi3.yml과 swagger의 연동
기존에 작성한 openapi3 설정에서 api spec이 정의된 파일을 외부에 보내고 읽을 수 있도록 outputDiretory를 지정합니다.
그러면 swagger.html파일에서 지정한 url은 openapi3.yaml파일을 읽게 되고,
아래와 같은 swagger UI에 데이터가 나타나는것을 확인할 수 있습니다.
UI가 이쁘지 마음에 들지 않다면 아래와 같이 커스터마이징해서 API를 오픈할 수 있습니다.
못생긴 AsciiDoc보다는 훨씬 낫습니다(?)
outputDirectory를 지정해서 생성된 파일들을 지정할 수 있으니, 분산된 환경에서도 문서화를 일괄적으로 관리할 수도 있게 됩니다.
테스트 커버리지를 한번만 100%를 달성해놓으면 그 이후에는 테스트코드를 통한 생성된 문서가 검증되었다는 안정감을 가지고 개발할 수 있게 됩니다.
더 나아가 분산된 환경에서도 일괄적으로 문서들을 관리할 수 있게 되니 큰 조직에서도 유용하게 사용할 수 있을 것이라고 생각됩니다.
발표내용과 같이 엄청난 퍼포먼스를 따라해보기는 어려웠지만, 작은 프로젝트를 생성하고 비교적 짧은 시간안에 해보려고 했던 기능들과 문서화를 만들어 보았습니다.
Mock과 RestAssured를 기반으로 기존에 테스트가 잘 진행되고 있는 환경이라면 쉽게 문서화와 테스트 커버리지를 개선해 볼 수 있을 것 같습니다.
이참에 한번 업무에서 시도해보는건 어떨까요?
데모를 만들면서 사용한 코드들은 github에 있습니다. step별로 branch가 있으니 코드 작업과정을 보실 수 있습니다.