웹 파이프라인/Acceptance

1218 인수테스트 자동화 - CI파이프라인 완성, TDD

thinktank911 2025. 12. 16. 17:54

인수테스트(UAT; User Acceptance Test)

  • 요구사항 대로 기능이 구현되었는지를 확인하는 과정
    • 전체 시스템 사용자 관점에서 시험하는 블랙박스 테스트
  • 사용자 인수 테스트를 자동화하는 것이 어려운 요인들
    • 스테이징 환경 : 프로덕션 환경과 동일한 스테이징(테스트) 환경에서 이뤄져야 함

도커 레지스트리의 구성

  • 빌드한 이미지를 저장 및 관리할 수 있는 저장소 셋업
  • CI/CD 과정과 k8s 클러스터의 운용에 대해 더 깊이 이해

어플리케이션 패키지 빌드 및 이미지 푸시

  • 소스 수준에서 테스트 (UT) 가 완료된 어플리케이션을 독립 및 통일된 환경에서 실행될 수 있도록 컨테이너화
  • 응용 소프트웨어를 (전체 의존성을) 컨테이너에 포함하여 이미지로 만들고 레지스트리에 저장

UAT 프레임워크 적용

  • 보다 체계적이고 완성도가 높은 인수 테스트를 위한 프레임워크 활용
    • 응용 소프트웨어의 개발 환경에 따라 크게 달라지는 부분이지만 예제를 중심으로 시나리오 이해

아티팩트 리포지토리

버전 관리, 접근 제어 등의 기능을 가지는 소프트웨어 개발 산출물(예:패키지)을 발행(푸시)하거나

인출(풀)할 수 있는 저장소 및 관리 기법이 필요

파이프라인의 모든 단계에서 동일한 바이너리가 사용되는 것을 보장함으로써 지속적 인도 프로세스에서 매우 중요한 역할

도커 레지스트리

  • 컨테이너화된 소프트웨어 산출물인 도커 이미지 관리할 수 있는 리포지토리
  • 클라우드 방식 레지스트리
    • Docker Hub
    • 상용 클라우드에서 제공하는 서비스(AWS ECR, GCP Artifact Registry...)
  • 자체 호스팅 방식 레지스트리
    • 사내 네트워크 아닌 외부에 소프트웨어 보관하는 것 금지하는 정책 갖고 있는 경우 유일한 해결법
    • 직접 관리의 부담이 있고 접근 제어 및 인증서 설정 등 번거로운 작업 수반

이미지 레지스트리 구축 실습

  • 도커 이용해 로컬 컴퓨터 컨테이너로 레지스트리 서비스 테스트
    • 도커 레지스트리가 도커 이미지로 제공되고 있으므로, 가져다가 실행하고 테스트
  • k8s 클러스터에 레지스트리 서비스 배포
    • 기본적 동작 테스트 : 클러스터 운용 실습
    • OpenSSL 인증서 설치, 설정
    • PV, PVC 만들어 레지스트리 데이터가 호스트에 남아 있도록 설정
    • docker push와 kubectl run으로 기본적 확인, Jenkins 파이프라인 테스트

데이터 볼륨과 SSL 인증서

  • 호스트 디렉토리를 레지스트리에 볼륨으로 공유
    • 레지스트리 저장된 데이터는 컨테이너 및 포드 등이 사멸하는 경우에도 유지 가능
    • PV 정의하고 PVC 설정하여 레지스트리 서버 컨테이너에서 이용핟록 볼륨 마운트
  • 자가 서명된(self-signed) 인증서 발급하여 레지스트리 서버에 설치
    • 실제 운용환경에서는 CA(certificate authority)로부터 발급받은 인증서 설치하고 주기적 갱신
      공인할 수 있는 인증을 이용할 것
    • 간이로 자가 서명 인증서 발급 후 레지스트리 서버에 설치

자가 서명 인증서 발급

  • OpenSSL을 이용한 인증서 발급 및 설치
    • openSSL 설치
    • certs 폴더 생성
    • k8s secret으로 등록
 openssl req `
>> -newkeys rsa:4096 -nodes -sha256 -keyout cert/registry.key `
>> -addext "subjectAltName = DNS:registry-service.registry.svc.cluster.local" `
>> -x509 -days 3650 -out certs/registry.crt
kubectl create secret tls registry-cert --cert=certs/registry.crt --key=certs/registry.key -n registry

Jenkins 파이프라인 인수테스트

1 개발자가 변경한 코드 GitHub에 푸시
2 Jenkins가 변경 감지 코드를 인출해 빌드 시작, 코드 점검(단위테스트 수행)
3 Jenkins가 빌드를 완료하여 도커 이미지 생성
4 Jenkins가 생성한 이미지를 레지스트리로 푸시
5 Jenkins가 스테이징 환경을 구성하고 도커 컨테이너 실행
6 스테이징 환경의 도커 호스트가 이미지 가져다가 (풀) 컨테이너 실행
7 Jenkins가 스테이징 환경에서 실행 중인 어플리케이션을 대상으로 인수 테스트 실행

 

 

Jenkins 에서 도커 이미지 빌드, 푸시 및 인수 테스트 환경 준비

  • 컨테이너 설정 필요

로컬 환경 빌드 및 테스트

  • Gradle 이용해 Java archive 파일 생성 (빌드)
  • Dockerfile 작성, 컨테이너 생성 실행
  • 위 컨테이너 대상으로 간단한 테스트 실행해 컨테이너의 정상 동작을 가볍게 확인

Jenkins 파이프라인에 스테이지 추가

  • 도커 이미지 빌드 및 푸시
  • 스테이징 환경에 컨테이너 실행, 인수 테스트 적용

테스트 자동화를 위한 리포지토리 설정

  • GitHub 소스에 두 파일 추가
    • Dockerfile
    • acceptance_test.sh : 자동화된 인수 테스트를 수행하기 위해 테스트 스크립트도 코드 리포지토리에 등록

작업용 임시 파이프라인 만들기

  • 기존에 설정한 파이프라인에서 "Poll SCM" 옵션 끄기
  • 시험 위한 새로운 Jenkins 아이템 생성
    • 리포지토리 Jenkins 내용 가져와 Pipeline Script로 입력
    • 단, Checkout 스테이지의 checkout scm은 이전에 이용했던 방식 적용해
      github 리포지토리 주소와 credentials 정보 적용한 내용을 변경
  • 지금까지 구성한 단계가 성공적으로 동작함 확인 (Build Nodw)
    • Checkout > Compile > Unit Test > Code Coverage > Static Analysis

스테이징 및 인수 테스트 파이프라인 추가

stage("Deploy to Staging"){
    steps {
        sh '''
            docker run -d --rm -p 8765:8080 --name calculator ${REGISTRY_URI}/calculator:${REGISTRY_TAG}
        '''
    }
}
stage("Acceptance Test"){
    steps {
        sleep 60
        sh '''
            chmod +x acceptance_test.sh
            && ./acceptance_test.sh
        '''
    }
}

테스트 통과 준비 완료

  • acceptance_test.sh 내용 올바른 결과 테스트하도록 수정
  • git commit -> git push
  • Build Now 눌러서 결과 확인

클린업 스테이지 추가

  • 스테이징 환경에 실행했던 도커 컨테이너를 삭제
  • 섹션 post에 "always"로 단계 추가
    • 빌드가 실패하더라도 반드시 실행되는 단계
post {
    always {
        sh '''
            docker stop calculator
        '''
    }
}

요약

  • 인수테스트
    • 프로덕션 환경과 동일한 바이너리(빌드의 결과)를 이용해 스테이징
    • 스테이징된 소프트웨어(컨테이너화되어 있음)를 대상으로 사용자 요구사항 만족하는지 테스트
  • Jenkins 이용한 인수 테스트 자동화
    • Dockerfile을 포함하여 빌드와 관련한 모든 파일 GitHub 소스 리포지토리에 등록
    • 자동화 테스트가 가능한 방법을 구현하고 이를 실행하는 파이프라인 스테이지 정의
  • 그러나 이 테스트가 인수테스트로서 적당하게 이용될 수 있는가?

인수테스트 프레임워크 적용

사용자 대면 테스트

  • REST API의 경우 curl에 의한 테스트로도 어느 정도 블랙박스 테스트 가능
  • 그러나 일반적으로 이런 테스트에 의존할 수 었음
  • 사용자와 함께 작성 가능 사용자가 이해할 수 있는 테스트 작성 방법 필요
    • 특정한 목적을 반영하는 인수테스트

BDD (Behavior-Driven Development)

  • 사용자가 인수 기준 제시
  • 위 인수 기준으로부터 개발자는 픽스처 또는 스템 정의라고 부르는 사용자 친화 DSL(Domain-specific language)와
    프로그래밍 언어 통합해 테스트 작성
  • 도구
    • Cucumber
    • Selenium

Cucumber 이용한 인수 테스트 생성

  • 인수 기준 생성
    • 특정 파일에 비즈니스 사양 명세
    • 경로 : \src\test\resources\feature
Feature: Calculator
  Scenario: Sum two numbers
    Given I have two numbers: 2 and 3
    When the calculator sums them
    Then I receive 5 as a result
  • 스텝 정의 작성
    • 기능 사양을 실행할 수 있는 Java 바인딩 생성
    • \src\test\java\acceptance
package acceptance;

import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import org.springframework.web.client.RestTemplate;

import static org.junit.Assert.assertEquals;

public class StepDefinitions {
    private String server = System.getProperty("calculator.url");

    private RestTemplate restTemplate = new RestTemplate();

    private String a;
    private String b;
    private String result;

    @Given("^I have two numbers: (.*) and (.*)$")
    public void i_have_two_numbers(String a, String b) throws Throwable {
        this.a = a;
        this.b = b;
    }

    @When("^the calculator sums them$")
    public void the_calculator_sums_them() throws Throwable {
        String url = String.format("%s/sum?a=%s&b=%s", server, a, b);
        result = restTemplate.getForObject(url, String.class);
    }

    @Then("^I receive (.*) as a result$")
    public void i_receive_as_a_result(String expectedResult) throws Throwable {
        assertEquals(expectedResult, result);
    }
}
  • 자동 인수 테스트 실행
    • Gradle 설정에 라이브러리 의존성 명세 추가
    • Gradle 태스크와 Junit 테스트 러너를 추가

Gradle 태스크 설정

// build.gradle

    testImplementation("io.cucumber:cucumber-java:7.14.0")
    testImplementation("io.cucumber:cucumber-junit:7.14.0")

tasks.named('test') {
    useJUnitPlatform()
}

tasks.register('acceptanceTest, Test'){
    include '**/acceptance/**'
    systemProperties System.getProperties()
}

test{
    useJUnitPlatform()
    exclude '**/acceptance/**'
}

Junit 테스트 러너를 추가

  • src\test\java\acceptance\AcceptanceTest.java
package acceptance;

import io.cucumber.junit.CucumberOptions;
import io.cucumber.junit.Cucumber;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:feature")

public class AcceptanceTest {

}

로컬 환경에서의 테스트 점검

 docker run -d --rm -p 8765:8080 localhost:30100/calculator:1.0
 ./gradlew acceptanceTest "-Dcalculator.url=http://localhost:8765"

테스트는 항상 실패하는 시나리오부터


TDD(Test-Driven Development)

인수 테스트는 기술보다 사람이 중심
TDD는 특히 인수테스트 시기에 적합
인수 기준 사양을 먼저 작성

  • 인수테스트 통과를 기능 구현 완료로 간주
  • Jira 등 이슈 추적 도구의 요청 티켓에 기능 사양 첨부하는 방법도 적용

요약 및 정리

  • 레지스트리 셋업
    • 클러스터 활용 실습 및 이미지 레지스트리 동작 기초 이해
  • 인수 테스트 위한 빌드 환경 구축
    • Jenkins agent 컨테이너의 구성 및 설정 실습
  • CI 파이프라인 완성
    • CD 파이프라인 구축의 토대가 될 기술적 요소의 학습
  • 테스트의 중요성, TDD
    • 특히 프로세스의 자동화를 통한 '지속적 인도' 관점에서 테스트의 중요성 이해