본문 바로가기
카테고리 없음

✅ 레거시 코드에서 TDD 적용하기 | 3편: 테스트 데이터베이스 연결과 통합 테스트 전략

by bbongz 2025. 3. 29.

 

이전 글에서는 외부 API와 데이터베이스 호출을 Moq을 활용하여 Mocking하는 방법을 소개했습니다. 하지만 일부 상황에서는 Mocking이 아닌, 실제 테스트용 데이터베이스 및 네트워크 환경에서 테스트를 수행해야 합니다. 특히 레거시 시스템에서는 의존성을 완전히 분리하기 어려워 Integration Test(통합 테스트)가 필수적인 경우가 많습니다.

이번 글에서는 실제 테스트 환경에서 테스트를 수행할 수 있도록, 테스트용 데이터베이스 구성통합 테스트 전략을 단계별로 설명합니다.


🧩 1. 통합 테스트가 필요한 이유

TDD의 핵심은 작은 단위의 코드(단위 테스트)를 통해 신뢰할 수 있는 프로그램을 만드는 것입니다. 그러나 실제 운영 환경에서는 여러 구성 요소가 유기적으로 작동합니다.

❗ Mock만으로는 테스트가 어려운 경우:

  • 외부 시스템(ERP, 대외 API 등)과의 실제 통신 확인
  • 대량 데이터 Insert/Delete/Update 시 성능 및 트랜잭션 확인
  • DB 스키마, 인덱스, 제약 조건 유효성 검증

👉 이럴 땐 실제 테스트 환경에서 실행하는 통합 테스트(Integration Test)가 필요합니다.


🛠️ 2. 테스트 데이터베이스 준비하기 (.NET Framework 기준)

✅ 운영환경과 분리된 테스트 전용 DB를 구축해야 합니다.

  1. SQL Server 기준 구성
    • TestDB라는 이름으로 테스트 전용 데이터베이스 생성
    • 운영 DB와 동일한 스키마 복제 (단, 테스트용 더미 데이터만 입력)
    • 테스트 전용 계정 생성 (test_user, 최소 권한 부여)
  2. App.config에 연결 문자열 설정
<connectionStrings>
  <add name="TestDbConnection" 
       connectionString="Server=localhost;Database=TestDB;User Id=test_user;Password=test_pw;" 
       providerName="System.Data.SqlClient"/>
</connectionStrings>

🔄 3. 테스트 전/후 데이터 초기화 전략

✅ 테스트의 독립성과 반복성을 보장해야 합니다.

MSTest의 TestInitialize, TestCleanup 사용

[TestClass]
public class DatabaseIntegrationTests
{
    private SqlConnection _connection;

    [TestInitialize]
    public void Init()
    {
        _connection = new SqlConnection(ConfigurationManager.ConnectionStrings["TestDbConnection"].ConnectionString);
        _connection.Open();

        var command = new SqlCommand("DELETE FROM Orders", _connection);
        command.ExecuteNonQuery();
    }

    [TestCleanup]
    public void Cleanup()
    {
        _connection.Close();
    }
}

👉 테스트 실행 전마다 데이터 초기화 → 테스트 간 간섭 방지


🧪 4. 실제 통합 테스트 코드 예제

다음은 테스트 DB에 연결해 주문 데이터를 저장하고 조회하는 Integration Test 예제입니다.

📌 대상 클래스: OrderRepository

public class OrderRepository
{
    private readonly string _connectionString;

    public OrderRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void SaveOrder(Order order)
    {
        using (var conn = new SqlConnection(_connectionString))
        {
            conn.Open();
            var command = new SqlCommand("INSERT INTO Orders (OrderId, TotalAmount) VALUES (@id, @amount)", conn);
            command.Parameters.AddWithValue("@id", order.OrderId);
            command.Parameters.AddWithValue("@amount", order.TotalAmount);
            command.ExecuteNonQuery();
        }
    }

    public Order GetOrder(int orderId)
    {
        using (var conn = new SqlConnection(_connectionString))
        {
            conn.Open();
            var command = new SqlCommand("SELECT OrderId, TotalAmount FROM Orders WHERE OrderId = @id", conn);
            command.Parameters.AddWithValue("@id", orderId);
            using (var reader = command.ExecuteReader())
            {
                if (!reader.Read()) return null;
                return new Order
                {
                    OrderId = reader.GetInt32(0),
                    TotalAmount = reader.GetDecimal(1)
                };
            }
        }
    }
}

📌 테스트 코드

[TestClass]
public class OrderRepositoryTests
{
    private OrderRepository _repository;

    [TestInitialize]
    public void Setup()
    {
        var connStr = ConfigurationManager.ConnectionStrings["TestDbConnection"].ConnectionString;
        _repository = new OrderRepository(connStr);

        using (var conn = new SqlConnection(connStr))
        {
            conn.Open();
            new SqlCommand("DELETE FROM Orders", conn).ExecuteNonQuery();
        }
    }

    [TestMethod]
    public void SaveOrder_And_GetOrder_Returns_Same_Order()
    {
        // Arrange
        var order = new Order { OrderId = 10, TotalAmount = 250.0m };

        // Act
        _repository.SaveOrder(order);
        var savedOrder = _repository.GetOrder(10);

        // Assert
        Assert.IsNotNull(savedOrder);
        Assert.AreEqual(order.OrderId, savedOrder.OrderId);
        Assert.AreEqual(order.TotalAmount, savedOrder.TotalAmount);
    }
}
  • 실제 DB와 연결된 테스트
  • 테스트 데이터 초기화 후 검증
  • 운영 DB와 격리된 환경에서 안전하게 테스트 가능

🚧 5. 주의사항 및 최적화 팁

항목 설명
테스트 속도 통합 테스트는 상대적으로 느림. 핵심 케이스만 테스트
테스트 격리 테스트마다 DB 초기화 필수
트랜잭션 처리 TransactionScope로 테스트 후 롤백 가능
CI/CD 통합 테스트 전용 DB를 자동 구성하는 스크립트 필요

🎯 결과 요약

  • 운영 DB와 분리된 테스트용 DB 설정 완료
  • 테스트 데이터 초기화 및 클린업 전략 적용
  • 실제 데이터 저장/조회 통합 테스트 구현
  • 테스트 간 독립성 확보

🚀 다음 편 예고

👉 다음 글에서는 레거시 코드 리팩토링을 통해 DI(Dependency Injection)을 적용하고, 테스트 커버리지 도구 활용테스트 코드 구조 정리 전략을 설명할 예정입니다.