study/server

[Ubuntu] 08. 홈 서버 운영 - Unit Test와 Integration Test 분리

렁치 2026. 8. 4. 21:28

서버 배포와 백업까지 정리한 뒤에는 테스트가 신경 쓰이기 시작했다.

처음에는 로컬에서 dotnet test가 통과하면 괜찮다고 생각했다. 그런데 Codex나 GitHub Actions 같은 환경에서 테스트를 돌리려고 하니 문제가 생겼다.

오류는 대략 이런 식이었다.

Cannot create an automatic instance.
Unknown error

처음에는 코드가 깨졌나 싶었는데, 자세히 보니 테스트 코드 자체의 실패가 아니라 LocalDB 자동 인스턴스를 만들지 못해서 생긴 문제였다.


문제 상황

현재 운영 DB는 Ubuntu 서버의 Docker MSSQL에 있다.

Ubuntu Server
    ↓
Docker
    ↓
mssql-db
    ↓
SurvivalGame DB

하지만 테스트가 운영 DB에 직접 붙으면 안 된다.

테스트는 데이터를 만들고 지우고, 경우에 따라 Migration을 적용하거나 DB를 초기화할 수도 있다. 운영 DB에 붙이면 실제 데이터를 망가뜨릴 수 있다.

그래서 테스트용 DB가 따로 필요하다.

처음에는 LocalDB를 사용할 수 있다고 생각했다. Windows 로컬에서는 흔한 방식이다.

테스트 실행
    ↓
LocalDB
    ↓
테스트용 DB 생성

문제는 LocalDB가 Windows 환경에 강하게 묶여 있다는 점이다.


LocalDB가 불편했던 이유

LocalDB는 로컬 Windows 개발 환경에서는 편하다. 하지만 아래 환경에서는 문제가 생길 수 있다.

GitHub Actions Linux Runner
Docker 컨테이너
Codex Sandbox
일부 제한된 Windows 세션

실제로 Codex 샌드박스에서는 LocalDB 자동 인스턴스를 만들지 못했다.

Cannot create an automatic instance.

나중에는 내 로컬 Windows의 LocalDB도 한 번 꼬였다. 결국 아래 명령으로 재생성해서 복구했다.

sqllocaldb stop MSSQLLocalDB
sqllocaldb delete MSSQLLocalDB
sqllocaldb create MSSQLLocalDB
sqllocaldb start MSSQLLocalDB
sqllocaldb info MSSQLLocalDB

정상 상태는 이렇게 나온다.

State: Running
Auto-create: Yes
Instance pipe name: ...

이걸 고치고 나니 로컬 전체 테스트는 다시 통과했다.

하지만 여기서 중요한 건 LocalDB를 고친 것이 끝이 아니라는 점이다. 테스트 구조 자체를 환경에 덜 의존하게 만들어야 했다.


테스트를 둘로 나누기로 했다

테스트를 크게 두 종류로 나눴다.

Unit Test
- 빠르게 실행
- DB 의존 없음
- 로컬 / Codex / CI에서 항상 실행 가능

Integration Test
- 실제 SQL Server 필요
- GitHub Actions의 MSSQL 컨테이너에서 실행
- main 또는 수동 검증용

처음에는 모든 테스트를 한 덩어리로 봤다. 그런데 실제로는 성격이 다르다.

데미지 계산, 권한 판정, 보상 계산 같은 로직은 DB 없이도 테스트할 수 있다.

반대로 Migration, Repository, 실제 HTTP 요청과 DB 저장까지 확인하는 테스트는 SQL Server가 필요하다.

두 테스트를 같은 방식으로 돌리면 빠른 테스트도 DB 환경 때문에 실패할 수 있다.


Trait으로 Integration 표시

xUnit에서는 Trait을 이용해 테스트를 분류할 수 있다.

DB가 필요한 테스트 클래스에는 아래 특성을 붙였다.

[Trait("Category", "Integration")]
public class MigrationFromScratchTests
{
    // 실제 SQL Server가 필요한 테스트
}

클래스 단위로 붙이는 편이 관리하기 쉬웠다.

DB 연결 필요
Migration 필요
WebApplicationFactory로 실제 API 실행
SQL Server 상태 확인

이런 테스트는 Integration으로 분류했다.


Unit Test만 실행하기

DB가 필요 없는 테스트만 실행할 때는 필터를 사용한다.

dotnet test .\SurvivalGame.sln --filter "Category!=Integration"

빌드와 복원을 분리하면 더 명확하다.

dotnet restore .\SurvivalGame.sln
dotnet build .\SurvivalGame.sln --no-restore
dotnet test .\SurvivalGame.sln --no-build --no-restore --filter "Category!=Integration"

이렇게 하면 테스트 실행 중 restore/build를 반복하지 않는다.

현재 Unit Test는 LocalDB 없이 정상 실행된다.

Unit Test
    ↓
DB 연결 없음
    ↓
빠르게 통과

Integration Test만 실행하기

Integration Test만 실행할 때는 반대로 필터를 건다.

dotnet test .\SurvivalGame.sln --filter "Category=Integration"

로컬에서는 LocalDB나 SQL Express가 정상이어야 돌릴 수 있다.

하지만 앞으로 기본 검증은 GitHub Actions의 MSSQL 컨테이너를 사용할 생각이다.

GitHub Actions
    ↓
MSSQL service container
    ↓
테스트 DB 생성
    ↓
Integration Test
    ↓
Runner 종료 후 삭제

운영 서버의 MSSQL에 붙이는 방식은 피했다.

운영 DB는 실제 서비스용이고, 테스트가 건드리면 안 된다.


GitHub Actions에서 MSSQL 컨테이너 사용

Integration job에서는 MSSQL 컨테이너를 service로 띄운다.

services:
  mssql:
    image: mcr.microsoft.com/mssql/server:2022-CU20-ubuntu-22.04
    env:
      ACCEPT_EULA: Y
      MSSQL_SA_PASSWORD: Test_password123!
    ports:
      - 1433:1433
    options: >-
      --health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Test_password123! -C -Q 'SELECT 1'"
      --health-interval 10s
      --health-timeout 5s
      --health-retries 10

테스트 연결 문자열은 환경변수로 넘긴다.

env:
  SURVIVALGAME_TEST_SQLSERVER: "Server=localhost,1433;User Id=sa;Password=Test_password123!;Encrypt=False;TrustServerCertificate=True;MultipleActiveResultSets=true"

그리고 Integration 테스트만 실행한다.

- name: Backend integration tests
  run: dotnet test ./SurvivalGame.sln --no-build --no-restore --filter "Category=Integration"

테스트 실행 정책

최종적으로 테스트 정책은 이렇게 잡았다.

로컬 개발 중
    → Unit Test 위주

DB 기능 변경 시
    → 로컬 Integration Test 또는 수동 Actions Integration

main 반영 시
    → Unit + Frontend 자동 실행

중요한 DB 변경 전
    → Integration 수동 실행

처음에는 main에 들어갈 때마다 Integration까지 자동으로 돌리게 했지만, Actions 사용량이 너무 빨리 늘었다. 그래서 나중에는 Integration을 수동 실행으로 바꿨다.

이 부분은 다음 글에서 따로 정리한다.


테스트 구조 다이어그램

                   ┌──────────────────────┐
                   │       dotnet test      │
                   └───────────┬──────────┘
                               │
               ┌───────────────┴────────────────┐
               │                                │
       Category != Integration          Category = Integration
               │                                │
        Unit Test                         Integration Test
               │                                │
   DB 없이 빠르게 실행              MSSQL 컨테이너 필요
               │                                │
 로컬 / Codex / CI 가능             GitHub Actions에서 검증

오늘 배운 점

테스트가 실패했다고 해서 항상 코드가 틀린 것은 아니었다.

이번에는 LocalDB 자동 인스턴스 생성 실패, SQL Express 인증 문제, Codex 환경 제약 같은 것들이 섞여 있었다. 처음에는 전부 테스트 실패로만 보였는데, 하나씩 나눠보니 코드 실패와 환경 실패를 분리할 수 있었다.

테스트도 서버 구조처럼 역할을 나눠야 한다는 걸 느꼈다.

빠른 검증은 가볍게
실제 DB 검증은 격리된 환경에서
운영 DB는 건드리지 않기

이 원칙만 잡아도 테스트가 훨씬 덜 불안해진다.


다음 작업

다음에는 GitHub Actions 사용 시간을 줄이는 작업을 정리한다.

처음에는 feature 브랜치, main, Integration, Deploy가 자주 돌면서 Actions 분을 많이 사용했다. Private 저장소에서는 GitHub Actions 시간이 무료 포함량을 직접 소모하므로 운영 방식도 조정해야 했다.