테스트 자동화에 필요성을 느껴 시도해보게 되었다.
기능 구현에 집중하다보면 테스트를 해야하는 사실을 잊어버려 실수하는 경우가 있어, 코드 변경으로 인해 잘 되던 기능이 동작하지 않게되는 경우가 있다. 이를 그대로 배포해버리면 작고 큰 사고로 이어질 수 있을 것이다.
테스트 자동화를 적절한 시점에 적용하면, 보다 신뢰할 수 있는 코드가 develop, main 브랜치에 병합될 수 있도록함으로써 어려움을 개선할 수 있다.
자동화는 husky와 같은 기술이나, github action 등의 기술로 달성할 수 있다. 이번에는 Github Action을 이용한 테스트 자동화 과정에 대한 글을 써내려가보려 한다.
Github 환경에서 테스트 자동화를 하게되면 여러 환경에서의 테스트, 협업 시 팀원들과 테스트 결과를 공유 및 피드백, PR에 대한 결과를 가독성있게 모아볼 수 있는 등의 이점을 가진다.
Github actions를 위한 테스트 자동화를 위해 우선 스크립트 작성부터 시작해보자.
테스트를 원하는 시점을 정하기 위해 해당 event와 타깃 브랜치를 작성해준다.
아래와 같이 main, develop 방향으로의 pull request에 변경사항이 있을 때 실행되도록 해주었다.
name: 기능 테스트
on:
pull_request:
branches: ['main', 'develop']
테스트를 실행할 원격 서버인 runner를 다음과 같이 지정해준다.
해당 서버에서 어떤 런타임 환경에서 실행할 것인지, 그리고 실행할 명령어들을 순차적으로 작성한다.
checkout, setup-node action을 사용하는데 각각 workflow에서 repo에 접근할 수 있게하고, node 기반 런타임 환경을 사용하기 위함이다.
name: 기능 테스트
on:
pull_request:
# main, develop 방향으로의 pull request가 올라올 때 실행
branches: ['main', 'develop']
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout to this repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: .nvmrc
이때 node-version을 .nvmrc로 지정했는데 이렇게 하면 repo에 저장된 .nvmrc를 참조하여 이 파일에 기록된 노드 버전을 사용하는 것으로 간주한다.
직접 매직넘버를 전달해도되며, 배열 형식으로 여러 값을 아래와 같이 전달하여 다른 버전의 환경에서의 실행을 할수도 있다.
name: 기능 테스트
on:
pull_request:
# main, develop 방향으로의 pull request가 올라올 때 실행
branches: ['main', 'develop']
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [ 14, 16, 18 ]
steps:
- uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
기본 환경을 세팅했으니, 실행할 스크립트를 작성해준다.
테스트 실행에 필요한 의존성을 설치해준 후, test 실행이 필요하다. 이때, 사전에 package.json의 scripts에서 test 명령어 실행 시 “vitest run”이 실행되도록 지정해주어야한다.
의존성 설치는 npm i
또는 npm ci
를 통해 수행한다. 보다 정확한 버전의 의존성 설치를 통해 버전 불일치로 인한 오류를 최소화하고 빠른 속도의 장점을 가진 npm ci
를 이용했다.
name: 기능 테스트
on:
pull_request:
# main, develop 방향으로의 pull request가 올라올 때 실행
branches: ['main', 'develop']
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout to this repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: .nvmrc
- name: Install dependencies from lock file
run: npm ci
- name: Run test
run: npm run test
의존성 설치까지는 정상적으로 되었으나 test 실행 과정에서 다음과 같이 Cannot find module
으로 인한 에러가 발생했다.
사전의 npm ci
단계에서 vite 기반 테스트 실행에 필요한 모듈이 설치되지 않아서이다.
이 명령어를 실행하면 package-lock.json을 참조하여 패키지 의존성들이 설치된다. 즉 local 개발 환경의 의존성을 그대로 설치하므로 @rollup/rollup-darwin-arm64를 설치하게된다. 하지만 테스트 환경은 실행을 위해 다른 모듈인 @rollup/rollup-linux-x64-gnu을 필요로한다.
실제 vite의 의존성인 rollup은 다음과 같은 optionalDependencies
에 의존하고 운영체제의 종류에 따라 가용한 모듈을 설치한다.
"node_modules/rollup": {
"version": "4.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.1",
"@rollup/rollup-android-arm64": "4.14.1",
"@rollup/rollup-darwin-arm64": "4.14.1",
"@rollup/rollup-darwin-x64": "4.14.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.1",
"@rollup/rollup-linux-arm64-gnu": "4.14.1",
"@rollup/rollup-linux-arm64-musl": "4.14.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.1",
"@rollup/rollup-linux-riscv64-gnu": "4.14.1",
"@rollup/rollup-linux-s390x-gnu": "4.14.1",
"@rollup/rollup-linux-x64-gnu": "4.14.1",
"@rollup/rollup-linux-x64-musl": "4.14.1",
"@rollup/rollup-win32-arm64-msvc": "4.14.1",
"@rollup/rollup-win32-ia32-msvc": "4.14.1",
"@rollup/rollup-win32-x64-msvc": "4.14.1",
"fsevents": "~2.3.2"
}
},
이를 해결할 수 있는 방법으로는npm ci
대신 npm i
를 통해 의존성을 설치하거나, npm ci
와 더불어 npm i @rollup/rollup-linux-x64-gnu@4.14.1
를 실행하는 두 가지가 있다.
나는 npm ci를 유지하고자 다음과 같이 추가 의존성 설치 명령어를 작성해주었고 어떤 동작인지 쉽게 파악가능하게 step 이름도 함께 작성해주었다.
name: 기능 테스트
on:
pull_request:
# main, develop 방향으로의 pull request가 올라올 때 실행
branches: ['main', 'develop']
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout to this repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: .nvmrc
- name: Install dependencies from lock file
run: npm ci
- name: Install additional dependencies
run: npm i @rollup/rollup-linux-x64-gnu@4.14.1
- name: Run test
run: npm run test
이제 기본적인 테스트 자동화 실행은 완성이 되었다.
main, develop 방향으로 pull request에 변경 사항이 있을 때 마다 테스트 자동화가 제대로 되고있는 것을 볼 수있다. 진행상황 및 실행결과는 repo의 Actions탭 또는 해당 PR의 checks list에서 확인할 수 있다.
해당 액션을 수행하는데만 44s 가 나왔다. 테스트 양도 몇없고, 의존성 수도 많은 게 아닌데 이렇게 걸린다면 더 큰 서비스에서는 이보다 몇 배는 더 필요할 것이고, 그동안 테스트 결과를 기다리는 시간이 무겁게 느껴졌다.
그래서 최적화를 위한 지원은 찾다가, 캐시 관련한 내용을 보았다.
캐시 액션을 적용하면, 기본적으로 워크플로 간에 기존에 설치된 의존성을 가져다가 쓰게 됨에 따라 액션 실행 속도가 빨라진다. 이때 캐시에 접근가능한 제약조건이 있어 혹시라도 캐시가 안된다 싶으면 이에 맞지않은 것일 수도 있다.
캐시를 사용하기 위해 setup-node action에서 제공하는 캐시 기능을 사용해 쉽고 빠르게 달성할 수 있다. 다음과 같이 with key의 cache key를 설정한다. npm, yarn 등의 값을 작성하여 지정된 패키지 매니저가 관리하는 의존성에 대한 캐시를 사용한다.
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: .nvmrc
cache: 'npm'
조금 더 섬세하게 캐시 기능을 구현하려면 github cache을 사용할 수 있다.
path는 의존성 설치 경로이고 key는 cache 저장될 키이다. 이렇게 설정함으로써 의존성 목록이 변경되지않는 한 캐시된 의존성을 가져와 사용하게된다.
name: 기능 테스트
on:
pull_request:
# main, develop 방향으로의 pull request가 올라올 때 실행
branches: ['main', 'develop']
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout to this repository
uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: .nvmrc
- name: Use Cache
id: 'cache-npm'
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-cache-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies from lock file
run: npm ci
- name: Install additional dependencies
run: npm i @rollup/rollup-linux-x64-gnu@4.14.1
- name: Run test
run: npm run test
첫 실행이라면 당연히 저장된 캐시가 없기 때문에 실행 시간의 변화가 없을 것이고 다음 실행부터 생성된 캐시를 사용하여 변화된 소요 시간을 볼 수 있을 것이다.
캐시가 성공적으로 사용되었다면 다음과 같은 출력을 볼 수 있다.
캐시를 찾았기 때문에 새롭게 저장하지 않는다. 만약 이후 의존성 목록이 변경되면 새로운 키를 갖는 캐시를 만들게 된다.
저장되어있는 캐시목록은 Actions 탭의 Management > Caches 에서 관리할 수 있다!
캐시의 공간은 한정적이기 때문에 이에 따라 miss가 발생하고 공간을 확보하는 추가작업 등으로 인해 액션의 속도가 느려질 수 있다. 이를 위해 캐시를 명시적으로 제거해줄 수 있다.
이에 대한 내용은 캐시 항목 강제 삭제에 설명이 잘 되어있다. 이를 따라 PR에서 파생된 캐시가 PR이 닫힐때 제거되어주도록 할 수 있었다.
vercel, netlify와 같은 클라우드 서비스를 사용하면 빌드 결과가 PR의 comment에 자동으로 등록되 듯, action 실행에 따른 테스트 결과도 자동화할 수 있다.
나는 PR에 결과를 출력함으로써 협업할 때, 어느 테스트 로직에 오류가 있는지 그리고 PR에 연관된 테스트 결과만 빠르게 접하고 이를 팀원들과도 공유하기 위한 목적으로 이 comment 기능이 필요하면 좋겠다고 생각했다. 포맷팅이 되어 한눈에 파악하기도 쉽다.
publish-unit-test-result-action을 사용했다. 해당 액션은 테스트 실행을 통해 출력된 파일 형태의 결과를 참조하여 PR comment에 테스트 결과를 요약하여 출력해준다.
동작을 위해서 job 내에 repo에 대한 권한을 아래과 같이 주고 step을 선언해준다.
permissions:
contents: read
issues: read
checks: write
pull-requests: write
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
check_name: 'test result'
files: |
test-results.xml # 사용하는 테스트 환경에서 출력하는 테스트 결과 경로와 일치하도록한다
vite.config.ts도 함께 설정해줘야한다. vitest에는 reporter기능을 제공한다. 다양한 포맷으로 테스트 결과물을 출력할 수 있지만 publish-unit-test-result-action액션은 JUnit XML만을 지원하여 아래와 같이 test 환경 설정을 해주었다.
// vite.config.ts
/// <reference types="vitest" />
import svgr from 'vite-plugin-svgr'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths(), svgr()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: 'setupEvent.js',
reporters: ['junit', 'default']
outputFile: 'test-results.xml',
},
})
설정이 끝난 후 다시 workflow를 실행하면 테스트 소요시간, 성공한 테스트 및 실패한 테스트 등의 요약된 테스트 정보를 PR comment에서 확인할 수 있다.
workflow 실행 시 테스트 실패에 대한 annotation을 changed file에서도 볼 수 있게 다음과 같이 설정할 수 있다.
// vite.config.ts
/// <reference types="vitest" />
import svgr from 'vite-plugin-svgr'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), tsconfigPaths(), svgr()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: 'setupEvent.js',
reporters: process.env.GITHUB_ACTIONS ? ['junit', 'default', 'github-actions'] : ['junit', 'default'],
outputFile: 'test-results.xml',
},
})
테스트가 실패하면 다음과 같은 에러 결과를 files changed에서 확인할 수 있다.
이외에도 Github repo 설정에서 제공하는 기능 중에서, test등의 check lists 의 일부가 실패했을 때 merge를 제한하도록 하는 기능을 통해 개발자의 실수를 줄일 수 있다
Vitest | Next Generation testing framework
GitHub Actions documentation - GitHub Docs
GitHub Actions의 캐시(Cache) 액션으로 패키지 설치 최적화하기 | Engineering Blog by Dale Seo
Github Actions 를 이용한 CI 테스트 자동화 — 화음을 좋아하는 리차드🎶