90DaysOfDevOps (Day 49)

고태규·2025년 11월 23일
0

DevOps

목록 보기
45/50
post-thumbnail

해당 스터디는 90DaysOfDevOps
https://github.com/MichaelCade/90DaysOfDevOps
를 기반으로 진행한 내용입니다.

Day 49 - From Confusion To Clarity: Gherkin & Specflow Ensures Clear Requirements and Bug-Free Apps


1. 테스트 피라미드


테스트는 CI/CD의 근본적인 기둥이며, 새로운 기능을 추가하거나 변경할 때 시스템이 망가지지 않고 기대대로 동작함을 보장해야 한다.

이를 효율적으로 수행하기 위해 테스트 피라미드 모델을 따르며, 각 테스트 단계는 비용과 속도 면에서 트레이드오프를 가진다.

  1. 단위 테스트 (Unit Test) - 피라미드의 기반

    • 특징: 피라미드의 가장 아래에 위치하며, 단일 클래스의 기능을 격리된 상태에서 테스트

    • 비용 및 속도: 구현 비용이 저렴하고 실행 속도가 가장 빠름.

    • 구현 방식: 클래스의 모든 의존성을 '모의 객체 (Mock)'나 '스텁 (Stub)'으로 대체하여, 오로지 해당 클래스만의 로직을 검증 (따라서 테스트의 수가 가장 많음)

  2. 통합 테스트 (Integration Test) - 구성 요소 간의 조화

    • 특징: 중간 단계에 위치하며, 여러 구성 요소 (Component)가 함께 작동할 때 기대한 대로 동작하는지를 검증

    • 비용 및 속도: 단위 테스트보다 구현 비용이 비싸고 실행 속도가 느림.

    • 구현 방식: 필요에 따라 일부 의존성을 모의 (Mock)할 수 있지만, 핵심은 컴포넌트 간의 상호작용 (단위 테스트보다는 수가 적음)

  3. 인수 테스트 (Acceptance Test) 및 종단 간 테스트 (E2E) - 최종 검증

    • 특징: 피라미드의 최상단에 위치하며, 애플리케이션을 블랙박스로 취급하여 시스템 전체를 관통하는 테스트를 수행한다. (예: 버튼을 눌러 결과 확인, API 요청 후 응답 확인)

    • 비용 및 속도: 구현 비용이 가장 높고 실행 속도가 가장 느림. (테스트 수는 가장 적음)

    • 구현 방식: 외부 API 같은 외부 의존성은 모의 (Mock)할 수 있지만, 데이터베이스와 같은 모든 내부 의존성은 실제로 구동시켜야 한다. 주로 컨테이너화된 환경을 이용


2. Gherkin (거킨)


개발자에게는 익숙한 코드이지만, 제품 관리자인 PO/PM나 비기술적인 이해관계자에게는 외계어일 수 있다.

이들과의 소통 간극을 줄이기 위해 거킨(Gherkin) 언어가 사용된다.

거킨은 평이한 영어로 작성되어 누구나 읽을 수 있으며, 이를 통해 비즈니스 요구사항을 명확한 테스트 시나리오로 변환할 수 있다.

거킨은 단위 테스트의 Arrange-Act-Assert 패턴과 유사한 Given-When-Then 구문을 따른다.

  • 거킨의 기본 구조 (은행 API 예시)
  1. Given (준비 - Arrange): 시스템의 초기 컨텍스트나 상태를 설정

    • 예시: "Given 내가 100달러의 잔액이 있는 계좌를 가지고 있을 때"

    • 활용: 로그인 상태 설정, 특정 권한 부여, 또는 외부 API 요청의 성공/실패 가정 등 테스트의 전제 조건을 정의

  2. When (실행 - Act): 사용자가 수행하는 실제 행동을 정의

    • 예시: "When 내가 50달러를 입금할 때"

    • 활용: 버튼 클릭, API 요청 전송 등 테스트하고자 하는 동작을 트리거

  3. Then (검증 - Assert): 행동에 따른 기대 결과를 검증

    • 예시: "Then 내 계좌 잔액은 150달러여야 한다"

    • 활용: 데이터 업데이트 확인, 화면 출력 확인, HTTP 응답 코드 (200, 403 등) 검증 등을 수행

추가적으로, 해당 단계들은 And 키워드를 통해 여러 조건을 연결하여 확장할 수 있으며, 텍스트 파일로 작성되므로 이메일로 공유하거나 메모장에서 편집하는 것도 가능하다.


3. SpecFlow와 데모


스펙플로우 (SpecFlow)는 작성된 거킨 시나리오를 .NET 코드와 연결해주는 프레임워크다.

거킨의 각 문장을 C# 메서드와 매핑하여, 빌드 파이프라인에서 자동으로 실행 가능한 테스트로 만들어준다.

데모로 사용된 '아재 개그API'의 실제 .feature 파일을 통해 어떻게 비즈니스 로직이 테스트 코드로 변환되는지 살펴보도록 하겠다.

# 데모에서 작성된 JokeTests.feature 파일
Feature: JokeTests
    Simple tests of the Dad Joke API

Scenario: Get a random joke
    Given a joke already exists
    When the endpoint for a random joke is called
    Then a joke should be returned

Scenario: Duplicate jokes cannot be added
    Given a joke already exists
    When the same joke is added again
    Then the joke should not be added
  1. 첫 번째 시나리오: 랜덤 개그 조회 (기본 기능)

    • Given (a joke already exists): 테스트를 시작하기 전, 시스템에 최소한 하나의 개그 데이터가 존재해야 함을 정의한다. 코드 내부적으로는 CreateJoke 메서드를 호출하여 데이터를 저장소에 미리 넣어둔다.

    • When (endpoint ... is called): 실제 API의 랜덤 조회 엔드포인트 (GET /api/jokes/random)를 호출한다.

    • Then (should be returned): 응답이 null이 아니며, 올바른 개그 객체가 반환되었는지 검증한다.


  1. 두 번째 시나리오: 중복 방지 (비즈니스 로직)

    이 시나리오는 단순 기능 확인을 넘어, 멱등성(Idempotency)이라는 비즈니스 규칙을 검증한다.

    • When (the same joke is added again): Given 단계에서 이미 생성된 개그와 동일한 내용으로 다시 생성 요청 (POST)을 보낸다. 이때 ScenarioContext에 저장해 둔 첫 번째 개그 데이터를 활용한다.

    • Then (should not be added): 시스템이 새로운 ID를 가진 개그를 생성하는 대신, 기존에 존재하던 개그 객체 (기존 ID)를 반환하는지 확인한다. 이를 통해 중복 저장이 방지되었음을 보증한다.


  1. 코드 연결 (Binding)

    스펙플로우는 위 거킨 문장들을 정규표현식을 통해 C# 메서드와 매핑한다.

    • Steps 클래스: [Binding] 어트리뷰트가 붙은 클래스 내에 [Given("a joke already exists")]와 같은 형태로 메서드를 정의한다.

    • ScenarioContext: Given 단계에서 생성한 개그의 ID나 객체를 ScenarioContext (키-값 저장소)에 저장해두고, When이나 Then 단계에서 이를 꺼내어 비교 검증에 사용한다.


4. Docker 컨테이너 활용


위 데모 코드는 Scenario: Get a random joke 등을 수행할 때 인메모리 저장소를 사용했지만, 실제 엔터프라이즈 환경에서는 데이터베이스와 같은 외부 시스템에 의존한다.

인수 테스트는 실제 DB 컨테이너를 띄워서 완벽하게 격리된 환경에서 수행해야 한다.

이를 위해 FluentDocker 라이브러리와 SpecFlow의 Hooks 기능을 사용한다.

Infrastructure Hooks: ApplicationHooks.cs
해당 코드는 테스트 실행 시 DB 컨테이너를 자동으로 띄우고, 테스트가 끝나면 정리하며, 각 시나리오마다 웹 서버를 초기화하는 코드이다.

using TechTalk.SpecFlow;
using Ductus.FluentDocker.Builders;
using Ductus.FluentDocker.Services;

[Binding]
public class ApplicationHooks
{
    private static IContainerService? _container;
    
    [BeforeTestRun]   // 1. [BeforeTestRun]: DB 컨테이너를 구동
    public static void BeforeTestRun()
    {
        _container = new Builder().UseContainer()
            .UseImage("kiasaki/alpine-postgres") 
            .ExposePort(5432) 
            .WithEnvironment("POSTGRES_PASSWORD=mysecretpassword") 
            .WaitForPort("5432/tcp", millisTimeout: 30000) 
            .Build()
            .Start(); 
    }

    [BeforeScenario] // 2. [BeforeScenario]: 웹 애플리케이션(SUT)을 띄우고 HttpClient를 생성하여 주입
    public void BeforeScenario(ObjectContainer objectContainer)
    {
        var webApplicationToTest = new WebApplicationToTest(); 
        var httpClient = webApplicationToTest.CreateClient();
        objectContainer.RegisterInstanceAs(httpClient);
    }

    [AfterTestRun] // 3. [AfterTestRun]: 리소스 정리 
    public static void AfterTestRun()
    {
        _container.Dispose();
    }
}
  • 컨테이너 구동 (BeforeTestRun):

    • 스펙플로우의 Hooks 클래스 내 [BeforeTestRun] 어트리뷰트가 붙은 메서드에서 FluentDocker 라이브러리를 사용한다.

    • 코드로 직접 PostgreSQL 등의 DB 컨테이너 이미지를 pull하고, 실행한다.

  • 설정 오버라이드 (ConfigureWebHost):

    • 테스트 실행 시 WebApplicationFactory를 통해 서버를 띄운다.

    • 이때 ConfigureWebHost 메서드를 오버라이드하여, 기존 appsettings.json의 DB 연결 문자열을 무시하고 방금 띄운 테스트용 도커 컨테이너의 연결 문자열로 교체한다.

    • 이를 통해 Scenario: Duplicate jokes cannot be added와 같은 테스트가 실제 DB 위에서 동작하게 된다.

  • 컨테이너 정리 (AfterTestRun):

    • 모든 시나리오 테스트가 끝나면 [AfterTestRun] 훅에서 컨테이너를 종료하고 삭제하여 환경을 깨끗하게 유지

해당 방식을 사용하면 개발자는 별도의 사전 환경 설정 없이도, 빌드 파이프라인 상에서 격리된 DB 환경을 자동으로 구축하고 테스트한 뒤 정리하는 완벽한 자동화를 구현할 수 있다.


5. 결론


이처럼 SpecFlow와 Docker를 결합하면 다음과 같은 자동화된 인수 테스트 파이프라인을 구축할 수 있다.

  • Given: 비즈니스 담당자가 읽을 수 있는 요구사항 명세 (Gherkin)가 있다.

  • Setup: 테스트 실행 시 자동으로 DB 컨테이너가 뜬다.

  • When/Then: 코드가 시나리오를 수행하고 결과를 검증한다.

  • Teardown: 테스트가 끝나면 환경이 깔끔하게 정리된다.

해당 과정은 개발자가 수동으로 환경을 세팅할 필요 없이 dotnet test 명령어 하나로 수행되며, CI/CD 파이프라인에 통합되어 배포 전 비즈니스 로직의 무결성을 보장하는 강력한 도구가 된다.


0개의 댓글