공부

[단위 테스트] JaCoCo + CI Github Actions 배지 생성하기

nahowo 2025. 2. 6. 00:07

 

JaCoCo 프로젝트 적용

  • build.gradle
  • plugins { id 'java' id 'org.springframework.boot' version '3.2.3' id 'io.spring.dependency-management' version '1.1.4' id 'org.hibernate.orm' version '6.4.4.Final' id 'org.graalvm.buildtools.native' version '0.9.28' id 'jacoco' } test { finalizedBy jacocoTestReport } jacocoTestReport { dependsOn test reports { xml.required = true html.required = true } } jacoco { toolVersion = "0.8.13" }
  • 위처럼 작성한 뒤 빌드 후 테스트를 수행했더니 아래와 같은 오류가 발생했다.
  • Execution failed for task ':test'. > Could not resolve all files for configuration ':jacocoAgent'. > Could not find org.jacoco:org.jacoco.agent:0.8.13. Required by: project : Possible solution: - Declare repository providing the artifact, see the documentation at <https://docs.gradle.org/current/userguide/declaring_repositories.html>
  • 더 찾아보고 추가로 build.gradle 파일을 작성해 주었다.
    • JaCoCo와 JDK 버전이 맞지 않는다는 글이 있어서 0.8.8로 버전을 변경했다.
    • Release note를 보니까 JDK 17은 0.8.8부터 지원한다고 한다.
  • plugins { id 'jacoco' } jacoco { toolVersion = "0.8.8" } repositories { mavenCentral() } test { useJUnitPlatform() finalizedBy jacocoTestReport } jacocoTestReport { dependsOn test reports { xml.required = true html.required = true } } dependencies { // Test testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' }
  • 그러면 천천히 설정을 살펴보고 커스텀해보자.

build.gradle 설정

  • repositories 공식 문서: https://docs.gradle.org/current/userguide/declaring_repositories.html
    • gradle은 프로젝트에서 사용할 dependencies를 다운로드받을 위치를 명시해야 한다. mavenCentral() 설정은 퍼블릭 메이븐 레포지토리에서 해당 dependencies를 다운받을 수 있다는 것을 의미한다.
    • 실제 퍼블릭 메이븐 레포지토리에 들어가 보니까 다운받을 수 있는 JaCoCo Agent 버전이 나열되어 있었다.
    • 오류 메시지는 Could not find org.jacoco:org.jacoco.agent:0.8.13. 인데, 버전 중 0.8.13은 없었다… 버전도 잘못되었고 어디서 다운받을지도 명시해주지 않아서 생긴 문제인 듯.
  • repositories { mavenCentral() }
  • test 공식 문서: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.testing.Test.html
    • test는 JUnit이나 TestNG 테스트를 실행한다.
    • useJUnitPlatform()은 JUnit 플랫폼 기반 테스트들을 탐색하고 실행하도록 명시한다.
    • Specifies that JUnit Platform should be used to discover and execute the tests.
    • finalizedBy 공식 문서: https://docs.gradle.org/current/kotlin-dsl/gradle/org.gradle.api/-task/finalized-by.html
      • 태스크 체이닝 방법 중 하나이다. finalizedBy [태스크] 라고 작성하면 해당 태스크 이후에 동작한다.
      • 추가로 dependsOn [태스크] 라고 작성하면 해당 태스크와 함께 동작한다.
  • test { useJUnitPlatform() finalizedBy jacocoTestReport }
  • jaCoCoTestReport - 태스크
    • 위에서 설명했듯이 test와 함께 동작한다.
    • reports는 ReportContainer의 인스턴스이다. 리포트의 반환 타입을 설명한다. required는 생성 여부를 결정하는 플래그이다(이전에는 enabled로 쓰였다고 한다. 여러 예시들에서 enabled만 나와 있길래 헷갈렸다).
    • xml은 현재 별로 필요가 없을 것 같아서 false로 변경해 두었다.
  • jacocoTestReport { dependsOn test reports { xml.required = true html.required = true } }
  • dependencies 공식 문서: https://docs.gradle.org/current/userguide/declaring_dependencies.html → 따로 정리가 필요할 것 같아서 포스트를 따로 작성었다.
  • dependencies { // Test testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0' }
  • 최종적인 build.gradle은 아래와 같다.
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'org.hibernate.orm' version '6.4.4.Final'
    id 'org.graalvm.buildtools.native' version '0.9.28'
    id 'jacoco'
}

jacoco {
    toolVersion = "0.8.12"
}

repositories {
    mavenCentral()
}

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport
}

jacocoTestReport {
    dependsOn test
    reports {
        xml.required = false
        html.required = true
    }
}

group = 'com.Alchive'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
    targetCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'mysql:mysql-connector-java:8.0.32'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.mapstruct:mapstruct:1.5.3.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'

    // OAuth
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

    // JWT
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // Swagger
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

    // Validation - @NotBlank
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // monitoring
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    
    // Slack API
    implementation 'com.slack.api:bolt:1.18.0'
    implementation 'com.slack.api:bolt-servlet:1.18.0'
    implementation 'com.slack.api:bolt-jetty:1.18.0'
    implementation 'com.slack.api:slack-api-client:1.44.1'

    // Discord API
    implementation 'net.dv8tion:JDA:5.0.0-beta.5'

//    // Test
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

hibernate {
    enhancement {
        enableAssociationManagement = true
    }
}

 

Github Actions CI

  • 엄청나게 오래 삽질하면서 작성한 CI 파이프라인…
name: CI/CD

on:
  push:
    branches:
      - develop
  pull_request:
    branches:
      - '**'

permissions:
  contents: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # 기본 체크아웃
      - name: Checkout
        uses: actions/checkout@v3
      # Gradlew 실행 허용
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      # JDK 17 세팅
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      # 환경 변수 설정
      - name: Set environment values
        run: |
          touch ./src/main/resources/env.properties
          echo "${{ secrets.ENV_PROPERTIES }}" > ./src/main/resources/env.properties
        shell: bash
      # 테스트 수행
      - name: Run Tests
        run: ./gradlew test
      # JaCoCo 배지 생성
      - name: Generate JaCoCo Badge
        uses: cicirello/jacoco-badge-generator/@v2
        with:
          generate-branches-badge: true
          jacoco-csv-file: build/reports/jacoco/test/jacocoTestReport.csv
      # JaCoCo 배지 깃허브 업로드
      - name: Upload JaCoCo Badge
        run: |
          if [[ -n "$(git status --porcelain .github/badges)" ]]; then
            git config --global user.name 'github-actions'
            git config --global user.email 'github-actions@github.com'
            git add .github/badges/*
            git commit -m "Update JaCoCo Badge"
            git push origin develop
          fi
      # Gradle build
      - name: Build with Gradle
        uses: gradle/gradle-build-action@v2
        with:
          arguments: clean build -x test

steps의 name 기준으로 과정을 설명한다.

  • 이전까지의 과정은 기존 CICD 과정과 동일하다.

Run Tests

  • ./gradlew test를 사용해 테스트를 돌린다.
    • 로컬 터미널에서 ./gradlew test 명령어를 수행하니까 알 수 없는 명령어라고 나왔다. 구글링을 해서 이 글을 보니까 os별 개행문자 차이 때문에 생기는 문제였다. 프로젝트 파일을 맥 로컬에서 직접 작성하지 않고 스프링 프로젝트 이니셜라이저로 하다 보니까 가끔 개행문자 오류가 생긴다…
    • 아무튼 vi gradlew로 파일을 열고, :set fileformat=unix를 작성한 뒤 저장하니까 해결되었다.
  • 로컬에서 잘 돌아가는지 확인한 뒤 커밋하는 것을 추천한다… 괜히 github actions 기다리고 커밋 메시지만 쌓이는 것보다 나음 ㅎㅎ…

Generate JaCoCo Badge

  • 나는 cicirello/jacoco-badge-generator를 깃허브 마켓플레이스에서 찾아 사용했다. 사용 방법은 깃허브 페이지에 자세히 나와 있다.
  • 분기 커버리지 배지를 생성하고, 스프링 JaCoCo 의존성에서 생성하도록 지정한 csv 파일의 위치도 명시한다.

Upload JaCoCo Badge

  • 처음에 위의 과정까지만 하니까 Actions 과정 내에서만 배지 이미지를 생성하고, 실제 내 프로젝트 레포지토리에는 배지 이미지가 생성되지 않았다. 같은 배지를 사용한 다른 레포지토리를 참조해서 배지 이미지를 커밋하도록 하는 로직을 추가했다.
  • 이렇게 ci를 다 작성했다! 전문은 이 링크에 있다.

배지 리드미에 업로드

  • 레포지토리에 업로드된 배지 이미지를 리드미에 추가해주기만 하면 된다. [![branches](.github/badges/branches.svg)] ← 아까 업로드했던 svg 파일의 경로를 지정한다.
  • 그러면 이렇게 커버리지 지표가 표시된다. 지금은 작성한 테스트 코드가 작아서 2.2%밖에 안 된다 ㅎㅎ…