diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3363fab..6b80d10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,30 +1,22 @@ -# 워크플로우의 이름을 지정합니다 -# 대시보드에서 워크플로우를 구분할 때 사용됩니다 name: CI Pipeline -# Event -on: # 워크플로우가 실행될 조건을 정의합니다 - pull_request: # PR을 조건으로 설정합니다 +on: + pull_request: branches: - - test # test 브랜치로 PR이 열리면 실행되도록 지정 + - test -# Jobs: 필요한 Job을 병렬 또는 (의존성에 의해)순차적으로 실행 -jobs: # 아래 계층에 수행할 Job을 정의합니다 +jobs: - # Build Job build: - runs-on: ubuntu-latest # 해당 Job의 러너 환경을 지정합니다 + runs-on: ubuntu-latest - # 아래에 build의 Step들을 정의합니다 steps: - # 1. 리포지토리에서 소스 코드를 체크아웃(복제) - - name: Checkout code # name으로 각 Step의 이름을 부여합니다 - uses: actions/checkout@v3 # uses를 통해 액션을 호출합니다 + - name: Checkout code + uses: actions/checkout@v3 - # 2. Gradle 라이브러리 캐싱 - name: Cache Gradle packages uses: actions/cache@v3 - with: # with를 통해 액션에 대한 추가적인 설정을 지정할 수 있습니다 + with: path: | ~/.gradle/caches ~/.gradle/wrapper @@ -32,29 +24,47 @@ jobs: # 아래 계층에 수행할 Job을 정의합니다 restore-keys: | ${{ runner.os }}-gradle- - # 3. Java 17 설치 - name: Set up JDK 17 uses: actions/setup-java@v3 with: distribution: 'temurin' java-version: '17' - # 4. gradlew에 권한 부여 - name: Grant execute permission for gradlew - run: chmod +x gradlew # run으로 직접 명령어를 실행할 수 있습니다 + run: chmod +x gradlew - # 5. 의존성 설치 및 빌드 - name: Build with Gradle run: ./gradlew build --no-daemon - # Test Job - # 빌드에서 테스트를 진행하기 때문에 해당 Job은 제외해도 됩니다. 그러나 needs를 설명하기 위해 편의상 추가했습니다. + # 추가 + - name: Leave a comment for test coverage + id: jacoco + uses: madrapps/jacoco-report@v1.2 + with: + title: 📊 Test Coverage Report + paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml # JaCoCo 리포트 위치 + token: ${{ github.token }} + min-coverage-overall: 80 # 전체 프로젝트의 커버리지가 80% 이상이어야 통과 + min-coverage-changed-files: 80 # 수정된 파일에 대한 최소 커버리지가 80% 이상이어야 통과 + pass-emoji: '✅' # 통과 이모지 (default : ":apple:") + # update-comment: true # true로 설정하는 경우 기존 코멘트를 업데이트하는 형태로 동작 + + # 슬랙 알림 추가 + - name: Slack message with build result + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took + if_mention: failure,cancelled + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + test: runs-on: ubuntu-latest - needs: build # build가 성공적으로 완료되어야 수행된다는 의미(의존성 설정) + needs: build steps: - # 기존 체크아웃, JDK 설치 과정 - name: Checkout code uses: actions/checkout@v3 @@ -64,6 +74,5 @@ jobs: # 아래 계층에 수행할 Job을 정의합니다 distribution: 'temurin' java-version: '17' - # 6. 테스트 실행 - name: Run tests run: ./gradlew test --no-daemon \ No newline at end of file diff --git a/README.md b/README.md index 0c4bd86..20176ed 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,24 @@ 2. [🗣️ GitHub Actions 소개](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EF%B8%8F-github-actions-%EC%86%8C%EA%B0%9C) 3. [🎯 GitHub Actions 주요 개념](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#-github-actions-%EC%A3%BC%EC%9A%94-%EA%B0%9C%EB%85%90) 4. [🚀 GitHub Actions 사용법](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#-github-actions-%EC%82%AC%EC%9A%A9%EB%B2%95) - * [대략적인 과정](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EB%8C%80%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B3%BC%EC%A0%95) - * [예시](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%98%88%EC%8B%9C) - * [`uses` 필드](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#uses-%ED%95%84%EB%93%9C) - * [`run` 필드](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#run-%ED%95%84%EB%93%9C) + * [대략적인 과정](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EB%8C%80%EB%9E%B5%EC%A0%81%EC%9D%B8-%EA%B3%BC%EC%A0%95) + * [예시](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%98%88%EC%8B%9C) + * [`uses` 필드](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#uses-%ED%95%84%EB%93%9C) + * [`run` 필드](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#run-%ED%95%84%EB%93%9C) 5. [🗃️ 캐싱(Caching)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EF%B8%8F-%EC%BA%90%EC%8B%B1caching) 6. [❌ 테스트 실패 살펴보기](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%A4%ED%8C%A8-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0) 7. [☑️ 조건문 사용하기](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EF%B8%8F-%EC%A1%B0%EA%B1%B4%EB%AC%B8-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0) 8. [💬 슬랙으로 알림 설정하기(Secrets 사용)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#-%EC%8A%AC%EB%9E%99%EC%9C%BC%EB%A1%9C-%EC%95%8C%EB%A6%BC-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0secrets-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0) - * [슬랙 채널 추가](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8A%AC%EB%9E%99-%EC%B1%84%EB%84%90-%EC%B6%94%EA%B0%80) - * [슬랙 앱 생성: Webhook URL 발급](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8A%AC%EB%9E%99-%EC%95%B1-%EC%83%9D%EC%84%B1-webhook-url-%EB%B0%9C%EA%B8%89) - * [시크릿 등록](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8B%9C%ED%81%AC%EB%A6%BF-%EB%93%B1%EB%A1%9D) - * [워크플로우 작성1: `slack-github-action`(버그)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B11-slack-github-action%ED%98%84%EC%9E%AC-%EB%B2%84%EA%B7%B8-%EC%9E%88%EC%9D%8C) - * [워크플로우 작성2: `slack-github-action`(버그 우회)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B12-slack-github-action%EB%B2%84%EA%B7%B8-%EC%9A%B0%ED%9A%8C) - * [워크플로우 작성3: `action-slack`](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B13-action-slack) + * [슬랙 채널 추가](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8A%AC%EB%9E%99-%EC%B1%84%EB%84%90-%EC%B6%94%EA%B0%80) + * [슬랙 앱 생성: Webhook URL 발급](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8A%AC%EB%9E%99-%EC%95%B1-%EC%83%9D%EC%84%B1-webhook-url-%EB%B0%9C%EA%B8%89) + * [시크릿 등록](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%8B%9C%ED%81%AC%EB%A6%BF-%EB%93%B1%EB%A1%9D) + * [워크플로우 작성1: `slack-github-action`(버그)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B11-slack-github-action%ED%98%84%EC%9E%AC-%EB%B2%84%EA%B7%B8-%EC%9E%88%EC%9D%8C) + * [워크플로우 작성2: `slack-github-action`(버그 우회)](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B12-slack-github-action%EB%B2%84%EA%B7%B8-%EC%9A%B0%ED%9A%8C) + * [워크플로우 작성3: `action-slack`](https://github.com/seungki1011/CICD-using-Github-Actions/tree/main?tab=readme-ov-file#%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C%EC%9A%B0-%EC%9E%91%EC%84%B13-action-slack) +9. [📊 JaCoCo를 이용한 코드 커버리지 추가]() + * [`build.gradle` 설정]() + * [워크플로우 추가]() +
@@ -40,11 +44,10 @@
-아래 브랜치는 `main`을 기반으로 합니다. - * **[`slack/slack-github-action-bug`](https://github.com/seungki1011/CICD-using-Github-Actions/tree/slack/slack-github-action-bug)**: `slackapi/slack-github-action@v1.27.0`을 사용합니다. 버그가 있습니다. * **[`slack/slack-github-action-workaround`](https://github.com/seungki1011/CICD-using-Github-Actions/tree/slack/slack-github-action-workaround)**: `slackapi/slack-github-action@v1.27.0`을 사용하지만, 버그를 우회한 방법입니다. * **[`slack/action-slack`](https://github.com/seungki1011/CICD-using-Github-Actions/tree/slack/action-slack)**: `8398a7/action-slack`을 사용합니다 +* **[`coverage/jacoco-report`](https://github.com/Madrapps/jacoco-report?tab=readme-ov-file)**: `Madrapps/jacoco-report`를 사용합니다. (슬랙 알림도 추가)
@@ -169,7 +172,7 @@ Github Action으로 워크플로우를 정의해서 사용하기 위해서는 --- -### 예시 1 +### 예시 워크플로우를 정의해서 사용하기 위해서 `.github/workflows/` 아래에 `ci.yml`이라는 파일을 만들어서 사용합니다. @@ -941,12 +944,199 @@ jobs:
+--- + +## 📊 JaCoCo를 이용한 코드 커버리지 추가 + +### `build.gradle` 설정 + +[JaCoCo (Java Code Coverage)](https://docs.gradle.org/current/userguide/jacoco_plugin.html)는 **Java 애플리케이션의 코드 커버리지를 측정하기 위한 오픈 소스 도구**입니다. 코드 커버리지는 단위 테스트나 통합 테스트에서 코드의 어느 부분이 실행되었는지를 나타내는 지표입니다. 이를 통해 작성된 테스트가 얼마나 효과적인지, 코드의 어느 부분이 테스트되지 않았는지 확인할 수 있습니다. + +JaCoCo를 이용해서 **코드 커버리지(code coverage)를 측정**하고, **해당 커버리지 리포트를 댓글(comment)로 달아주는 워크플로우를 추가** 해봅시다. + +
+ +먼저 JaCoCo를 사용하기 위해서 `build.gradle`에 필요한 설정을 추가해줍니다. + +```groovy +plugins { + // ... + id 'jacoco' // 추가 +} + +jacoco { + toolVersion = "0.8.10" // jacoco 버전 명시 +} + +jacocoTestReport { + reports { + xml.required = true // madrapps/jacoco-report를 사용하기 위해서 xml 리포트 사용 + html.required = true + } + + // 각 리포트 타입 마다 저장 경로를 설정할 수 있다 + // html.destination file("$buildDir/jacoco/html") + // xml.destination file("$buildDir/jacoco/xml") + + // xml 기본 저장 경로: $buildDir/reports/jacoco/test/jacocoTestReport.xml + + // 보고서에 표시되는 걸 제외하고 싶은 클래스를 명시 + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect { + fileTree(dir: it, excludes: [ + // 나의 프로젝트에 맞게 변경 {그룹이름}/{프로젝트명} + "seungki/cicdpractice/api/domain/**", + "**/*Application*", + "**/*Request*", + "**/*Response*", + "**/*Exception*" + ]) + }) + ) + // 보고서를 생성하고 나서야 테스트 커버리지 검증을 진행 + finalizedBy(jacocoTestCoverageVerification) +} + +// 커버리지 검증을 위한 기준 제시 +jacocoTestCoverageVerification { + violationRules { + rule { + + /** + * element: 커버리지를 체크하는 기준 + * + * BUNDLE: 전체 프로젝트의 모든 파일(default) + * CLASS: 클래스 + * METHOD: 메서드 + * PACKAGE: 패키지 + * SOURCEFILE: 소스 파일 + **/ + enabled = true + element = 'CLASS' + + /** + * counter: 커버리지 측정을 위한 최소의 단위 + * + * BRANCH: 조건문의 분기 수 + * CLASS: 클래스의 수 + * COMPLEXITY: 복잡도 + * INSTRUCTION: Java 바이트코드 명령의 수(default) + * METHOD: 메서드의 수 + * LINE: 빈 줄을 제외한 실제 코드의 라인 수 + **/ + + /** + * value: 커버리지의 측정 메트릭(metric) + * + * TOTALCOUNT: 전체 개수 + * MISSEDCOUNT: 커버되지 않은 개수 + * COVEREDCOUNT: 커버된 개수 + * MISSEDRATIO: 커버되지 않은 비율. 0 ~ 1 사이의 숫자로, 1이 100%. + * COVEREDRATIO: 커버된 비율. 0 ~ 1 사이의 숫자로, 1이 100%. (default) + **/ + + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.80 // value에 대한 최소 통과 기준 + } + + // 커버리지 체크를 제외할 클래스를 명시 + // jacocoTestReport과 가르게 패키지 경로를 적어줘야 한다 + excludes = [ + "seungki.cicdpractice.api.domain.**", + "**.*Application*", + "**.*Request*", + "**.*Response*", + "**.*Exception*" + ] + } + } +} + +tasks.named('test') { + useJUnitPlatform() + finalizedBy jacocoTestReport // 항상 테스트가 완료되어야 리포트 생성 +} +``` + +
+ +`.gradlew test` 또는 인텔리제이의 Gradle 도구에 들어가서 `Tasks>verification>test`를 실행해봅시다. + +
+ +![cifail2](./img/README/gradletest.png) + +

Gradle > Tasks > verification > test

+ +
+ +다음과 같은 결과를 얻었습니다. + +
+ +![cifail2](./img/README/coveragefail.png) + +![cifail2](./img/README/failreport.png) + +

service 커버리지의 최소 조건을 만족하지 못한다

+ +현재 컨트롤러 계층의 테스트만 작성했기 때문에, **서비스 계층에 대한 커버리지 조건을 만족하지 못하여 실패**했습니다. 서비스에 대한 **테스트를 추가하고 다시 시도**해보겠습니다. + +
+ +![cifail2](./img/README/jacocosuccess.png) + +

최소 기준을 만족하고, 통과된다

+ +
+ +--- + +### 워크플로우 추가 + +이번에는 [`madrapps/jacoco-report`](https://github.com/Madrapps/jacoco-report)를 사용해서 **코드 커버리지에 대한 리포트를 자동으로 댓글로 달아주는 스텝을 추가**해봅시다. + +
+ +```yaml +# 추가 +- name: Leave a comment for test coverage + id: jacoco + uses: madrapps/jacoco-report@v1.2 + with: + title: 📊 Test Coverage Report + paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml # JaCoCo 리포트 위치 + token: ${{ github.token }} + min-coverage-overall: 80 # 전체 프로젝트의 커버리지가 80% 이상이어야 통과 + min-coverage-changed-files: 80 # 수정된 파일에 대한 최소 커버리지가 80% 이상이어야 통과 + pass-emoji: '✅' # 통과 이모지 (default : ":apple:") + # update-comment: true # true로 설정하는 경우 기존 코멘트를 업데이트하는 형태로 동작 +``` + +
+ + + + + --- ## 📑 Reference -1. [https://pozafly.github.io/dev-ops/cache-and-restore-keys-in-github-actions/](https://pozafly.github.io/dev-ops/cache-and-restore-keys-in-github-actions/) -2. [https://fe-developers.kakaoent.com/2022/220106-github-actions/](https://fe-developers.kakaoent.com/2022/220106-github-actions/) -3. [https://hyperconnect.github.io/2021/11/08/github-actions-for-everyone-1.html](https://hyperconnect.github.io/2021/11/08/github-actions-for-everyone-1.html) -4. [https://github.com/8398a7/action-slack](https://github.com/8398a7/action-slack) -5. [https://github.com/slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) +* **GitHub Actions 설정** + * [https://pozafly.github.io/dev-ops/cache-and-restore-keys-in-github-actions/](https://pozafly.github.io/dev-ops/cache-and-restore-keys-in-github-actions/) + * [https://fe-developers.kakaoent.com/2022/220106-github-actions/](https://fe-developers.kakaoent.com/2022/220106-github-actions/) + * [https://hyperconnect.github.io/2021/11/08/github-actions-for-everyone-1.html](https://hyperconnect.github.io/2021/11/08/github-actions-for-everyone-1.html) + +* **Slack Notification 설정** + * [https://github.com/8398a7/action-slack](https://github.com/8398a7/action-slack) + * [https://github.com/slackapi/slack-github-action](https://github.com/slackapi/slack-github-action) + +* **JaCoCo 설정** + * [https://techblog.woowahan.com/2661/](https://techblog.woowahan.com/2661/) + * [https://docs.gradle.org/current/userguide/jacoco_plugin.html](https://docs.gradle.org/current/userguide/jacoco_plugin.html) + * [https://cl8d.tistory.com/119](https://cl8d.tistory.com/119) + * [https://github.com/Madrapps/jacoco-report?tab=readme-ov-file](https://github.com/Madrapps/jacoco-report?tab=readme-ov-file) diff --git a/build.gradle b/build.gradle index 18e76ca..86c0d1e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.3' id 'io.spring.dependency-management' version '1.1.6' + id 'jacoco' } group = 'seungki' @@ -17,6 +18,95 @@ repositories { mavenCentral() } +jacoco { + toolVersion = "0.8.10" // jacoco 버전 명시 +} + +jacocoTestReport { + reports { + xml.required = true // madrapps/jacoco-report를 사용하기 위해서 xml 리포트 사용 + html.required = true + } + // 각 리포트 타입 마다 저장 경로를 설정할 수 있다 + // html.destination file("$buildDir/jacoco/html") + // xml.destination file("$buildDir/jacoco/xml") + + // xml 기본 저장 경로: $buildDir/reports/jacoco/test/jacocoTestReport.xml + + // 보고서에 표시되는 걸 제외하고 싶은 클래스를 명시 + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect { + fileTree(dir: it, excludes: [ + "seungki/cicdpractice/api/domain/**", + "**/*Application*", + "**/*Request*", + "**/*Response*", + "**/*Exception*" + ]) + }) + ) + // 보고서를 생성하고 나서야 테스트 커버리지 검증을 진행 + finalizedBy(jacocoTestCoverageVerification) +} + +// 커버리지 검증을 위한 기준 제시 +jacocoTestCoverageVerification { + violationRules { + rule { + + /** + * element: 커버리지를 체크하는 기준 + * + * BUNDLE: 전체 프로젝트의 모든 파일(default) + * CLASS: 클래스 + * METHOD: 메서드 + * PACKAGE: 패키지 + * SOURCEFILE: 소스 파일 + **/ + enabled = true + element = 'CLASS' + + /** + * counter: 커버리지 측정을 위한 최소의 단위 + * + * BRANCH: 조건문의 분기 수 + * CLASS: 클래스의 수 + * COMPLEXITY: 복잡도 + * INSTRUCTION: Java 바이트코드 명령의 수(default) + * METHOD: 메서드의 수 + * LINE: 빈 줄을 제외한 실제 코드의 라인 수 + **/ + + /** + * value: 커버리지의 측정 메트릭(metric) + * + * TOTALCOUNT: 전체 개수 + * MISSEDCOUNT: 커버되지 않은 개수 + * COVEREDCOUNT: 커버된 개수 + * MISSEDRATIO: 커버되지 않은 비율. 0 ~ 1 사이의 숫자로, 1이 100%. + * COVEREDRATIO: 커버된 비율. 0 ~ 1 사이의 숫자로, 1이 100%. (default) + **/ + + limit { + counter = 'LINE' + value = 'COVEREDRATIO' + minimum = 0.80 // value에 대한 최소 통과 기준 + } + + // 커버리지 체크를 제외할 클래스를 명시 + // jacocoTestReport과 가르게 패키지 경로를 적어줘야 한다 + excludes = [ + "seungki.cicdpractice.api.domain.**", + "**.*Application*", + "**.*Request*", + "**.*Response*", + "**.*Exception*" + ] + } + } +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' @@ -27,10 +117,12 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - testCompileOnly 'org.projectlombok:lombok:1.18.34' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' +// testCompileOnly 'org.projectlombok:lombok:1.18.34' +// testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' } tasks.named('test') { useJUnitPlatform() + finalizedBy jacocoTestReport // report is always generated after tests run } +} \ No newline at end of file diff --git a/img/README/coveragefail.png b/img/README/coveragefail.png new file mode 100644 index 0000000..c6887ce Binary files /dev/null and b/img/README/coveragefail.png differ diff --git a/img/README/failreport.png b/img/README/failreport.png new file mode 100644 index 0000000..ce39a15 Binary files /dev/null and b/img/README/failreport.png differ diff --git a/img/README/gradletest.png b/img/README/gradletest.png new file mode 100644 index 0000000..829a358 Binary files /dev/null and b/img/README/gradletest.png differ diff --git a/img/README/jacocosuccess.png b/img/README/jacocosuccess.png new file mode 100644 index 0000000..c800ad8 Binary files /dev/null and b/img/README/jacocosuccess.png differ diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..8f7e8aa --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/src/main/java/seungki/cicdpractice/api/domain/Post.java b/src/main/java/seungki/cicdpractice/api/domain/Post.java index 3d88ca8..e415549 100644 --- a/src/main/java/seungki/cicdpractice/api/domain/Post.java +++ b/src/main/java/seungki/cicdpractice/api/domain/Post.java @@ -4,6 +4,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.util.Objects; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -27,4 +28,21 @@ public Post(Long id, String title, String content) { this.title = title; this.content = content; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Post post = (Post) o; + return Objects.equals(id, post.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } } diff --git a/src/test/java/seungki/cicdpractice/api/service/PostServiceTest.java b/src/test/java/seungki/cicdpractice/api/service/PostServiceTest.java new file mode 100644 index 0000000..38df30e --- /dev/null +++ b/src/test/java/seungki/cicdpractice/api/service/PostServiceTest.java @@ -0,0 +1,63 @@ +package seungki.cicdpractice.api.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import seungki.cicdpractice.api.domain.Post; +import seungki.cicdpractice.api.dto.reponse.PostResponse; +import seungki.cicdpractice.api.dto.request.PostCreateRequest; +import seungki.cicdpractice.api.repository.PostRepository; + + +@ExtendWith(MockitoExtension.class) +class PostServiceTest { + + @Mock + private PostRepository postRepository; + + @InjectMocks + private PostService postService; + + @Test + @DisplayName("유효한 데이터로 새로운 포스트를 성공적으로 생성한다") + void test1() { + + PostCreateRequest request = PostCreateRequest.builder() + .title("title test 1") + .content("content test 1") + .build(); + Post post = Post.builder() + .title("title test 1") + .content("content test 1") + .build(); + when(postRepository.save(any(Post.class))).thenReturn(post); + + PostResponse postResponse = postService.createPost(request); + + assertThat(postResponse).isNotNull(); + assertThat(postResponse.getTitle()).isEqualTo("title test 1"); + assertThat(postResponse.getContent()).isEqualTo("content test 1"); + + /** + * Mockito는 기본적으로 객체의 동등성을 비교하기 위해서 equals() 메서드를 사용한다 + * 만약 equals()를 오버라이드하지 않는다면 기본적으로 참조를 통해 비교하게 된다 + * equals(), hashCode() 오버라이딩이 필요하다 + */ + verify(postRepository, times(1)).save(post); + + // 오버라이딩 하지 않는 경우, 인자로 전달되는 객체의 속성만 검증하는 방법을 사용할 수 있다 +// verify(postRepository, times(1)).save(any(Post.class)); + + } + +} \ No newline at end of file