[Spring] JaCoCo 적용하여 테스트 커버리지 체크하기

6 분 소요

서론

테스트 코드는 개발 시간을 굉장히 단축시켜주는 강력한 도구입니다. 물론 코드를 작성하는데 시간도 많이 들어갈 뿐더러, 눈에 보이는 성과를 주지 않는 코드이기 때문에, 괜히 작성하는 시간이 아깝고 귀찮아서 잘 작성하지 않는 경우가 대부분입니다. 저도 마찬가지였구요… 기능이 적을때는 서버를 다시 시작해서 눈으로 확인하는게 더 편하다고 생각할 수도 있겠지만, 기능이 많아지거나, 과정이 복잡한 기능의 경우, 일일히 서버를 올린뒤 해당 기능에 에러가 날만한 값을 하나하나 넣으며 테스트하는 것은 몹시 비효율적인 행위입니다.

심지어 테스트 코드가 없다면 기능을 일부분 수정하거나, 기존 코드를 리팩토링 할 때도, 만약 문제가 발생한다면 어느 부분에서 발생한 것인지 알아채기도 어렵고, 아까 서술했던 방식대로 리팩토링을 하고나서 또 일일히 테스트를 해야할 것 입니다.

따라서 테스트 코드 작성을 꼼꼼히 해야하는데, 이를 강제해주는 좋은 라이브러리가 있어 한번 알아보려고 합니다.

  • 라이브러리의 이름은 JaCoCo입니다.

JaCoCo란?

JaCoCo는 자바 코드의 커버리지를 체크하는 라이브러리로, 테스트 코드가 현재 프로덕션코드의 얼마만큼 작성되었는지 퍼센테이지로 확인하도록 해주는 라이브러리 입니다. 만약 커버리지가 100퍼센트라면, 모든 프로덕션 코드에 대해서 테스트 코드가 작성되어 있는 상태라고 할 수 있을 것 입니다.

심지어 JaCoCo는 이렇게 커버리지 결과를 알려줄뿐만 아니라, 해당 커버리지가 사용자가 설정한 퍼센테이지에 미치지 못하면 build자체가 되지않게 설정하여, 테스트코드 작성을 강제합니다.

그럼 한번 JaCoCo를 어떻게 적용하는지 알아보고, 예제를 사용해보며 JaCoCo의 사용법을 알아 보겠습니다.

JaCoCo 추가하기

먼저, build.gradle에 JaCoCo를 설정해 줍니다. plugins부분에 아래와 같은 부분을 추가해주면, Gradle이 알아서 해당 의존성을 추가해줍니다.

1
2
3
plugins {
	id 'jacoco'
}

이렇게 JaCoCo의 의존성을 추가하고 나면 별도의 설정이 필요한데요, 아까 말한 커버리지의 퍼센테이지를 설정하고, 해당 리포트를 어떤 형식으로 저장할 지를 설정해주어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
jacocoTestReport {
  reports {
    html.enabled true
    xml.enabled false
    csv.enabled false

// 리포트의 저장 경로를 설정합니다.
html.destination file("jacoco/jacocoHtml")
xml.destination file("jacoco/jacoco.xml")
  }
}

jacocoTestCoverageVerification {
  
  violationRules { // 커버리지의 범위와 퍼센테이지를 설정합니다.
    rule {
      element = 'CLASS'

      limit {
        counter = 'BRANCH'
        value = 'COVEREDRATIO'
        minimum = 0.90
      }
    }
  }
}

이 설정 부분은 지금은 그냥 복사해서 붙여넣으시면 됩니다. 자세한 세팅은 뒤에서 다시 설명하겠습니다.

이렇게 하면 JaCoCo가 추가되는데, 오른쪽에 있는 gradle탭을 눌러 Tasks > Verification에 들어가면 아래와 같은 jcocoTestReportjacocoTestCoverageVerification 명령이 생긴 것을 확인 할 수 있습니다.

image1

그럼 이제 JaCoCo를 사용하기에 앞서서 테스트를 위한 간단한 프로덕션 코드를 작성해보고, 이를 테스트 해보겠습니다.

JaCoCo를 사용하여 테스트 코드 작성하기

클래스 이름은 Jacoco로 대략 붙이도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Jacoco{
    public String select(String name) {
        switch (name) {
            case "딸기":
                return "빨간색입니다.";
            case "바나나":
                return "노란색입니다.";
            default:
                return "잘 모르겠습니다.";
        }
    }

    public String giveMeFruit() {
        return "과일주세요!";
    }
}

간단한 함수 select()를 하나 추가 해주었는데요. 메서드에 인자로 딸기를 넘기면 딸기의 색깔을, 바나나를 넘기면 바나나의 색깔을 말해주는 메서드입니다. 만약 두 과일 외의 다른 과일이 온다면, ‘잘 모르겠습니다’ 라는 문구를 출력할 것 입니다.

그럼 이 메서드의 테스트 코드를 한번 작성해볼까요?

우선 간단하게 아래처럼 작성해보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
class JacocoTest {
    private Jacoco jacoco = new Jacoco();

    @Test
    public void 딸기_색깔을_잘_출력하는지_테스트() {
        String actual = jacoco.select("딸기");
        String expected = "빨간색입니다.";
        assertThat(actual).isEqualTo(expected);
    }
}

테스트 메서드는 알아보기 쉽게 이름을 한글로 지어보았습니다.

이렇게 하면 assertThat이 프로그램의 실제값(actual)우리가 기대하는 값(expected)가 같은 지 확인하여, 같다면 이 테스트가 pass될 것이고, 다르다면 fail될 것입니다.

그럼 이렇게 작성한 테스트 코드는 현재, 프로덕션 코드의 몇퍼센트를 커버하고 있을까요? 이를 알아보기 위해서 이제 JaCoCo를 작동시켜, 현재 테스트 코드의 커버리지를 확인해보도록 합시다.

위의 Gradle 탭에서 test,jcocoTestReportjacocoTestCoverageVerification 명령을 순서대로 작동시켜 봅시다.
각각의 명령은 테스트 코드를 실행(test)하고, 리포트를 생성(jacocoTestReport)한 다음, 커버리지를 체크(jacocoTestCoverageVerification) 합니다.

근데 아마 jacocoTestCoverageVerification 명령을 실행시키면, 에러가 뜨며 해당 명령이 정상적으로 실행되지 않았다고 나올 것 입니다.

image1

에러를 읽어보면, 우리의 테스트코드는 현재 0.33의 커버리지를 갖고있는데, 커버리지 기댓값은 0.90이라고 나와있습니다.
앞서 말했던대로, JaCoCo는 우리가 설정해둔 커버리지 값에 테스트 코드가 미치지 못하면 빌드 자체를 에러를 띄웁니다.

그럼 아까 build.gradle 파일에서 jcocoTestReport에 설정해준 경로,

1
html.destination file("jacoco/jacocoHtml")

에 생성된 index.html 파일을 열어 자세한 정보를 알아보겠습니다.

image1

index.html 파일을 원하는 브라우저로 열면, 커버리지 퍼센트를 확인 할 수 있습니다. (인텔리제이의 경우 오른쪽 위에 뜨는 팝업에서 브라우저를 누르면 바로 열 수 있습니다.)

image1

보면 Instruction은 55퍼센트의 커버리지를 달성했고, 전체 Branches는 33퍼센트의 커버리지 밖에 달성하지 못했습니다.

자세히 보기 위해서 com.jacoco.example을 눌러 세부정보들을 확인해 봅시다.

image1

위 사진을 보면 실행파일인 JacocoApplication과 우리가 방금 만들어준 Jacoco의 커버리지가 클래스별로 나와 있는 것을 볼 수 있습니다. 우선 제쳐두고 한번 더 Jacoco를 눌러서 들어가 한번 더 세부내용을 확인해 보도록 하겠습니다.

image1

메서드 별로 커버리지가 나와있는걸 확인 할 수 있습니다.

select() 함수의 경우 Missed Branches 의 커버리지가 33퍼센트인데, 여기서 ‘브랜치’는 어떤 범위를 이야기 하는 것 일 까요? index.html에서 select() 메서드를 눌러서 들어가 한번 코드를 확인해 봅시다.

image1

초록색, 노란색, 빨간색이 있는 걸 확인 할 수 있습니다. 빨간색의 경우는 커버하지 못한 부분, 노란색의 부분은 커버되긴 했으나, 100퍼센트 커버되지 못한 부분, 초록색의 부분은 완벽하게 커버된 부분입니다.

그럼, 이 메서드의 경우 결과값이 몇개의 분기로 이루어져 있을까요? 간단하게 return값을 3개 갖고있는 함수이므로, 3개입니다.

아까 우리의 테스트 코드를 보면, “딸기”를 입력하고 “빨간색입니다”의 출력이 나오는 경우만 테스트 코드를 작성했기 때문에, 바나나를 입력했을 때와 그 외의 입력값을 입력했을 때, 이 프로그램이 정상적으로 작동하는지 확신할 수 없는 상태일 것입니다. return 값을 확인해보면, “빨간색입니다.”의 return 값은 초록색으로 커버되었지만, 나머지 두 return값은 빨간색으로 커버되지 못한 것을 확인 할 수 있습니다. 따라서 Branch는, 이 메서드가 가질 수 있는 출력값의 개수임을 알 수 있습니다.

그렇다면 Instruction은 어떤 의미로 범위를 나눈 것인 걸까요?? Instruction은 지나간 바이트 코드를 측정합니다. 만약, 100개의 바이트 코드중에서 20개의 바이트코드를 커버했다면, Instruction은 20퍼센트인 것이지요. 무슨 말인지 모르겠다면, 간단하게 코드 글자 하나하나의 수라고 생각하면 편합니다. (정확한 설명은 아니지만, 대략 이렇게 알아두고 넘어가도 괜찮습니다.)

그럼 한번 select() 함수의 커버리지를 100퍼센트 달성하는 테스트 코드를 작성해볼까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class JacocoTest {
    private Jacoco jacoco = new Jacoco();

    @Test
    public void 딸기_색깔을_잘_출력하는지_테스트() {
        String actual = jacoco.select("딸기");
        String expected = "빨간색입니다.";
        assertThat(actual).isEqualTo(expected);
    }

    @Test
    public void 바나나_색깔을_잘_출력하는지_테스트() {
        String actual = jacoco.select("바나나");
        String expected = "노란색입니다.";
        assertThat(actual).isEqualTo(expected);
    }

    @Test
    public void 그_외의_값을_잘_출력하는지_테스트() {
        String actual = jacoco.select("키위");
        String expected = "잘 모르겠습니다.";
        assertThat(actual).isEqualTo(expected);
    }
}

마찬가지로 간편하게 알아보기 쉽게 메서드 이름을 한글로 지었습니다.

이로써 select() 메서드가 출력할 수 있는 결과인 세가지의 경우의 테스트 코드를 모두 작성하였습니다. 이제 리팩토링을 하거나 다른 함수가 추가 되어도, 우리는 이 테스트 코드가 통과한다면 select() 메서드는 여전히 정상적으로 동작하고 있음을 알 수 있을 것 입니다. 마찬가지로 다시 JaCoCo의 명령을 실행 시킨 후 select() 메서드의 테스트 코드 커버리지를 확인해볼까요?

image1

select() 메서드의 커버리지가 100퍼센트가 된 것을 확인 할 수 있습니다!

그럼 이제 Jacoco 클래스에 있는 또 다른 메서드인 giveMeFruit() 메서드도 테스트 코드를 작성하여, JaCoCo클래스의 커버리지를 100퍼센트로 달성해 볼까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class JacocoTest {
    private Jacoco jacoco = new Jacoco();

    @Test
    public void 딸기_색깔을_잘_출력하는지_테스트() {
        String actual = jacoco.select("딸기");
        String expected = "빨간색입니다.";
        assertThat(actual).isEqualTo(expected);
    }

    @Test
    public void 바나나_색깔을_잘_출력하는지_테스트() {
        String actual = jacoco.select("바나나");
        String expected = "노란색입니다.";
        assertThat(actual).isEqualTo(expected);
    }

    @Test
    public void 그_외의_값을_잘_출력하는지_테스트() {
        String actual = jacoco.select("키위");
        String expected = "잘 모르겠습니다.";
        assertThat(actual).isEqualTo(expected);
    }

    @Test
    public void giveMeFruit_테스트() {
        String actual = jacoco.giveMeFruit();
        String expected = "과일주세요!";
        assertThat(actual).isEqualTo(expected);
    }
}

image1

다음과 같이 Jacoo 클래스가 100퍼센트의 커버리지를 달성 한 것을 확인 할 수 있습니다.

그런데, 아직 이 프로젝트 전체의 커버리지를 확인해보면,

image1
image1

위 사진처럼 Branch는 100퍼센트의 커버리지를 달성했지만, Instruction은 100퍼센트가 달성되지 않은 것을 확인 할 수 있습니다. 물론 우리의 목표는 Branch 커버리지의 90퍼센트 이상으로 설정하여 이 상태로도 빌드는 진행될 것 입니다. 그래도 만약 혹여 테스트가 불가능 하거나, 위 JacocoApplication 파일처럼 테스트가 필요하지 않은 부분을 커버리지에서 제외해야 할 경우가 있으니, 이를 커버리지에 포함 시키지 않는 방법도 알아보도록 하겠습니다.

방법은 아주 간단합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import lombok.Generated;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Generated
@SpringBootApplication
public class JacocoApplication {

	public static void main(String[] args) {
		SpringApplication.run(JacocoApplication.class, args);
	}

}

Lombok 라이브러리의 Generated 어노테이션을 붙이면, 해당 클래스를 커버리지에서 제외합니다. 메서드 위에 붙여도 동일하게 작동합니다.

image1

이로써, 해당 프로젝트의 테스트 코드 커버리지 100퍼센트를 달성하였습니다.

카테고리:

업데이트:

댓글남기기