Bash 스크립트를 효과적으로 테스트하는 방법

엄격 모드, 추적, Bats, shUnit2, 모의 명령어, 임시 디렉토리, ShellCheck 및 CI 자동화를 사용하여 Bash 스크립트를 테스트합니다.

Bash 스크립트를 효과적으로 테스트하는 방법

Bash 스크립트는 종종 파일, 서비스, 배포 및 프로덕션 데이터를 다룹니다. Bash 스크립트를 효과적으로 테스트하면 정리 작업이 잘못된 디렉토리를 삭제하거나 배포 스크립트가 실패한 명령을 건너뛰기 전에 잘못된 가정을 발견할 수 있습니다.

시작하기 위해 거대한 프레임워크가 필요하지 않습니다. 방어적인 셸 옵션, 정적 검사, 집중된 단위 테스트 및 임시 테스트 환경을 결합하여 스크립트가 크고 예측 가능하게 실패하도록 만드십시오.


기본 사항: 방어적 코딩 및 디버깅

공식적인 단위 테스트를 구현하기 전에, 버그에 대한 첫 번째 방어선은 스크립트 자체의 구조에 있습니다. 엄격한 운영 설정을 활용하면 미묘한 런타임 오류를 즉각적인 실패로 전환하여 디버깅을 더 쉽게 만들 수 있습니다.

필수 방어적 헤더

많은 프로덕션 Bash 스크립트는 더 엄격한 옵션으로 시작합니다:

#!/bin/bash
# 명령이 0이 아닌 상태로 종료되면 즉시 종료합니다.
set -e

# 설정되지 않은 변수를 대체할 때 오류로 처리합니다.
set -u

# 파이프라인의 오류가 마스킹되지 않도록 방지합니다.
set -o pipefail

이것들을 set -euo pipefail로 결합하는 것이 일반적입니다. set -e는 조건문, 서브셸 및 파이프라인에서 예외가 있으므로 엄격 모드가 테스트를 대체한다고 가정하지 말고 예상되는 실패를 명시적으로 확인하십시오.

추적을 통한 수동 디버깅

빠른 디버깅 또는 스크립트 실행 흐름 이해를 위해 Bash는 내장된 추적 기능을 제공합니다:

  • 명령 추적 (-x): 명령과 해당 인수가 실행될 때 + 접두사와 함께 출력합니다.
  • 실행 없음 (-n): 명령을 읽지만 실행하지는 않습니다(구문 오류 확인에 유용).

스크립트를 실행할 때 또는 스크립트 내부에서 추적을 활성화할 수 있습니다:

# 추적과 함께 스크립트 실행
bash -x ./my_script.sh

# 특정 섹션에 대해 스크립트 내에서 추적 활성화
echo "복잡한 작업 시작..."
set -x # 추적 활성화
complex_function_call arg1 arg2
set +x # 추적 비활성화
echo "작업 완료."

공식 단위 테스트 프레임워크 채택

복잡한 로직에는 수동 디버깅이 지속 가능하지 않습니다. 공식 단위 테스트 프레임워크를 사용하면 반복 가능한 테스트 케이스를 정의하고, 예상 결과를 주장하며, 검증 프로세스를 자동화할 수 있습니다.

1. Bats (Bash 자동화 테스트 시스템)

Bats는 Bash 테스트를 위한 가장 인기 있고 쉬운 프레임워크입니다. 익숙한 Bash 구문을 사용하여 테스트를 작성할 수 있으며, 주장을 간단하고 읽기 쉽게 만듭니다.

Bats의 주요 기능:

  • 테스트는 Bash와 유사한 구문으로 작성됩니다.
  • 간단한 run 명령을 사용하여 대상 스크립트/함수를 실행합니다.
  • $status, $output$lines와 같은 내장된 주장 변수를 제공합니다.

예제: 간단한 함수 테스트

calculate_sum 함수를 포함하는 스크립트(calculator.sh)가 있다고 가정해 보겠습니다.

calculator.sh 스니펫:

calculate_sum() {
  if [[ $# -ne 2 ]]; then
    echo "오류: 두 개의 인수가 필요합니다" >&2
    return 1
  fi
  echo $(( $1 + $2 ))
}

test/calculator.bats:

#!/usr/bin/env bats

# 테스트할 함수가 포함된 스크립트를 소싱합니다.
# BATS_TEST_DIRNAME은 이 테스트 파일이 포함된 디렉토리를 가리킵니다.
source "$BATS_TEST_DIRNAME/../calculator.sh"

@test "유효한 입력은 올바른 합계를 반환해야 합니다" {
  run calculate_sum 10 5
  # 함수가 성공 상태(0)를 반환했는지 확인
  [ "$status" -eq 0 ]
  # 출력이 예상과 일치하는지 확인
  [ "$output" = "15" ]
}

@test "입력이 누락되면 오류 상태(1)를 반환해야 합니다" {
  run calculate_sum 5
  [ "$status" -ne 0 ]
  [ "$status" -eq 1 ]
  # 최신 bats-core 버전에서는 `run`을 사용할 때 stderr를 사용할 수 있습니다.
  # [ "$stderr" = "오류: 두 개의 인수가 필요합니다" ] 
}

테스트를 실행하려면:

bats test/calculator.bats

2. ShUnit2

ShUnit2는 xUnit 스타일의 테스트를 따르므로 Python 또는 Java와 같은 언어에서 온 개발자에게 친숙합니다. 프레임워크 파일을 소싱해야 하며 엄격한 명명 규칙(setUp, tearDown, test_...)을 준수해야 합니다.

ShUnit2의 주요 기능:

  • 정리를 위한 설정 및 해제 루틴을 지원합니다.
  • 풍부한 내장 주장 함수 세트를 제공합니다(예: assertTrue, assertEquals).

ShUnit2 구조

#!/bin/bash
# shUnit2를 소싱합니다. 설치에 따라 이 경로를 조정하십시오.
. /usr/local/share/shunit2/shunit2

# 변수/픽스처 정의

setUp() {
  # 각 테스트 전에 실행할 코드
  TEMP_FILE=$(mktemp)
}

tearDown() {
  # 각 테스트 후에 실행할 코드 (정리)
  rm -f "$TEMP_FILE"
}

test_basic_addition() {
  local result
  # 테스트 중인 함수 호출
  result=$(my_script_function 1 2)
  
  # 주장 함수 사용
  assertEquals "3" "$result"
}

# shUnit2 패키지가 끝에 명시적 소싱을 요구하는 경우,
# 상단 근처 대신 테스트 함수 후에 소싱하십시오.

Bash 스크립트 테스트 모범 사례

효과적인 테스트는 프레임워크 실행을 넘어서 구성 요소의 신중한 격리와 환경 종속성 관리가 필요합니다.

1. 입력, 출력 및 오류 처리

테스트는 표준 스트림(stdout, stderr)과 Bash에서 성공 또는 실패를 알리는 기본 메커니즘인 최종 종료 코드를 확인해야 합니다.

  • 종료 코드: 성공의 경우 status -eq 0, 구문 분석 실패 또는 파일 누락과 같은 오류 조건의 경우 0이 아닌 값을 테스트합니다.
  • 표준 출력 (stdout): 일반적으로 기본 데이터 출력입니다. Bats의 $output을 사용하거나 ShUnit2에서 출력을 캡처하여 정확성을 확인합니다.
  • 표준 오류 (stderr): 오류, 경고 및 디버깅 메시지는 여기로 라우팅되어야 합니다. 중요한 것은 성공적인 실행 중에 프로덕션 스크립트가 stderr에서 조용해야 한다는 것입니다.

2. 종속성 격리 (모킹)

단위 테스트는 외부 시스템 도구(예: curl, kubectl 또는 git)가 아닌 코드를 테스트해야 합니다. 스크립트가 외부 명령에 의존하는 경우 테스트 중에 해당 명령을 모의(mock) 처리해야 합니다.

방법: 실제 종속성과 동일한 이름을 가진 모의 실행 파일이 포함된 임시 디렉토리를 만듭니다. 테스트를 실행하기 전에 이 디렉토리를 $PATH 앞에 추가하여 스크립트가 실제 도구 대신 모의를 호출하도록 합니다.

모의 예제:

#!/bin/bash
# 파일: /tmp/mock_bin/curl

if [[ "$1" == "--version" ]]; then
  echo "모의 Curl 7.6"
  exit 0
else
  # 성공적인 API 응답 시뮬레이션
  echo '{"status": "ok"}'
  exit 0
fi

테스트 설정에서:

export PATH="/tmp/mock_bin:$PATH"

3. 임시 환경을 사용한 통합 테스트

통합 테스트는 스크립트가 파일 시스템 및 운영 체제와 올바르게 상호 작용하는지 확인합니다. 시스템을 오염시키거나 다른 테스트를 방해하지 않도록 임시 디렉토리를 사용하십시오.

mktemp 사용

mktemp -d 명령은 안전하고 고유한 임시 디렉토리를 생성합니다. 테스트 실행 중에 모든 파일 조작(생성, 수정, 정리)을 이 디렉토리 내에서 수행해야 합니다.

setUp() {
  # 이 테스트 실행을 위한 임시 디렉토리 생성
  TEST_ROOT=$(mktemp -d)
  cd "$TEST_ROOT"
}

tearDown() {
  # 임시 디렉토리 정리
  cd - >/dev/null
  rm -rf "$TEST_ROOT"
}

@test "스크립트는 필요한 로그 파일을 생성해야 합니다" {
  run my_script_that_writes_logs
  
  # 임시 디렉토리에 예상 파일이 존재하는지 확인
  [ -f "./log/script.log" ]
}

4. 이식성 테스트

Bash 구현은 약간 다릅니다(예: GNU Bash vs. macOS/BSD Bash). 이식성이 중요한 경우 Docker 컨테이너를 사용하여 다양한 대상 환경에서 테스트 스위트를 실행하여 유틸리티 명령 또는 매개변수 확장의 미묘한 차이를 발견하십시오.

워크플로우에 테스트 통합

테스트는 사후 고려 사항이 되어서는 안 됩니다. 테스트 스위트를 버전 관리 및 CI/CD(지속적 통합/지속적 배포) 파이프라인에 통합하십시오.

  1. 버전 관리: 소스 스크립트와 함께 테스트 디렉토리(예: test/)를 저장합니다.
  2. 사전 커밋 훅: shellcheck(정적 분석 도구) 및 포맷터와 같은 도구를 사용하여 커밋 전에 코드 품질을 보장합니다.
  3. CI 자동화: CI 서버(GitHub Actions, GitLab CI, Jenkins)를 구성하여 푸시할 때마다 Bats 또는 ShUnit2 테스트 스위트를 자동으로 실행합니다. 테스트가 0이 아닌 상태를 반환하면 빌드를 실패 처리합니다.

경고: shellcheck와 같은 정적 분석 도구는 단위 테스트의 훌륭한 동반자입니다. 테스트가 놓칠 수 있는 일반적인 실수, 이식성 문제 및 보안 취약점을 발견합니다. 사전 테스트 루틴의 일부로 항상 shellcheck를 실행하십시오.

결론

shellcheckset -euo pipefail로 시작한 다음, 입력을 구문 분석하고, 파일을 선택하고, 외부 도구를 호출하거나, 되돌릴 수 없는 변경을 수행하는 스크립트 부분 주변에 테스트를 추가하십시오. 모의 종속성 및 임시 디렉토리가 있는 작은 Bats 스위트는 종종 위험한 스크립트를 자신 있게 변경할 수 있는 자동화로 전환하기에 충분합니다.