테스트 전략 선택

개요에서 설명한 대로 애플리케이션과 마찬가지로 테스트에 프로덕션 데이터베이스 시스템이 포함되는지 또는 테스트가 프로덕션 데이터베이스 시스템을 대체하는 테스트 더블에 대해 실행되는지 여부를 먼저 결정해야 합니다.

테스트 더블로 대체하는 대신 실제 외부 리소스에 대한 테스트를 수행하면 다음과 같은 어려움을 수반할 수 있습니다.

  1. 대부분의 경우 실제 외부 리소스에 대해 테스트하는 것은 불가능하거나 실현할 수 없습니다. 예를 들어 애플리케이션은 속도 제한 또는 테스트 환경 부족으로 인해 쉽게 테스트할 수 없는 일부 서비스와 상호 작용할 수 있습니다.
  2. 실제 외부 리소스를 포함할 수 있더라도 진행이 지나치게 느려질 수 있습니다. 클라우드 서비스에 대해 많은 양의 테스트를 실행하면 테스트가 상당히 오래 걸릴 수 있습니다. 테스트는 개발자의 일상적인 워크플로에 속해야 하므로 테스트를 신속하게 실행해야 합니다.
  3. 외부 리소스에 대한 테스트를 실행하면 테스트가 서로 간섭하는 격리 문제를 수반할 수 있습니다. 예를 들어 데이터베이스에 대해 병렬로 실행되는 여러 테스트는 데이터를 수정하여 서로가 다양한 방식으로 실패할 수 있습니다. 테스트 더블을 사용하면 각 테스트가 자체 메모리 내 리소스에 대해 실행되어 다른 테스트와 자연스럽게 격리되므로 이를 방지할 수 있습니다.

그러나 테스트 더블에 대해 통과한 테스트여도 실제 외부 리소스에 대해 실행할 때 프로그램이 작동하지 않을 수 있습니다. 예를 들어 데이터베이스 테스트 더블은 대/소문자를 구분하는 문자열 비교를 할 수도 있는 반면 프로덕션 데이터베이스 시스템은 대/소문자를 구분하지 않는 비교를 합니다. 이러한 문제는 실제 프로덕션 데이터베이스에 대해 테스트가 실행될 때만 발견되므로 해당 테스트는 모든 테스트 전략의 중요한 과정입니다.

데이터베이스에 대한 테스트가 예상보다 쉬울 수 있음

실제 데이터베이스에 대한 테스트와 관련하여 위의 어려움 때문에 개발자는 테스트 더블을 먼저 사용하고 컴퓨터에서 자주 실행할 수 있는 강력한 테스트 제품군을 갖추는 것이 훨씬 좋습니다. 반면 데이터베이스와 관련된 테스트는 가급적 적게 실행해야 하며 많은 경우 적용 범위도 훨씬 적습니다. 일반적으로는 후자에 대해 더 많이 고민하는 것이 좋으며, 데이터베이스가 실제로 생각보다 위 문제에 대해 영향을 많이 받지 않을 수도 있다고 안내합니다.

  1. 오늘날 대부분의 데이터베이스는 개발자의 컴퓨터에 쉽게 설치할 수 있습니다. Docker와 같은 컨테이너 기반 기술을 사용하면 더욱 쉬워지며 Github 작업 영역개발자 컨테이너와 같은 기술은 데이터베이스를 포함한 전체 개발 환경을 설정합니다. SQL Server를 사용하면 Windows에서 LocalDB에 대해 테스트하거나 Linux에서 Docker 이미지를 쉽게 설정할 수도 있습니다.
  2. 적절한 테스트 데이터 세트를 사용하여 로컬 데이터베이스에 대해 수행하는 테스트는 일반적으로 매우 빠릅니다. 통신은 완전히 로컬이며 테스트 데이터는 일반적으로 데이터베이스 측의 메모리에 버퍼링됩니다. EF Core 자체에는 SQL Server에 대해 30,000개가 넘는 테스트가 있습니다. 이러한 테스트는 몇 분 안에 안정적으로 완료되고, 매 단일 커밋에서 CI로 실행되며, 개발자가 로컬에서 실행하는 경우가 아주 많습니다. 일부 개발자는 이것이 속도에 필요하다고 생각하여 메모리 내 데이터베이스(‘모조’)로 전환합니다. 실제로는 거의 그렇지 않습니다.
  3. 실제 데이터베이스에 대해 테스트를 실행할 때 테스트가 데이터를 수정하고 서로 간섭할 수 있으므로 사실상 격리는 장애물입니다. 그러나 데이터베이스 테스트 시나리오에서는 격리를 제공하는 다양한 기술이 있습니다. 프로덕션 데이터베이스 시스템에 대한 테스트에서 중점적으로 설명합니다.

위 내용은 테스트 더블을 폄하하거나 사용을 반대하려는 것이 아닙니다. 우선 데이터베이스 오류 시뮬레이션과 같이 다른 방식으로 테스트할 수 없는 일부 시나리오에는 테스트 더블이 필요합니다. 하지만 지난 경험에 따르면 사용자는 위의 이유로 데이터베이스가 느리거나, 어렵거나, 불안정하다고 생각하여 데이터베이스에 대한 테스트를 꺼리는 경우가 많지만 꼭 그렇지는 않습니다. 프로덕션 데이터베이스 시스템에 대한 테스트는 데이터베이스에 대해 빠르고 격리된 테스트를 작성하기 위한 지침과 샘플을 제공하여 이 문제를 해결하는 것을 목표로 합니다.

다양한 테스트 더블 형식

테스트 더블은 매우 다양한 접근 방식을 아우르는 광범위한 용어입니다. 이 섹션에서는 EF Core 애플리케이션을 테스트하기 위한 테스트 더블과 관련된 몇 가지 일반적인 기술에 대해 설명합니다.

  1. SQLite(메모리 내 모드)를 모조 데이터베이스로 사용하여 프로덕션 데이터베이스 시스템을 대체합니다.
  2. EF Core 메모리 내 공급자를 모조 데이터베이스로 사용하여 프로덕션 데이터베이스 시스템을 대체합니다.
  3. DbContextDbSet을 모의 또는 스텁합니다.
  4. EF Core와 애플리케이션 코드 사이에 리포지토리 레이어를 도입하고 해당 레이어를 모의 또는 스텁합니다.

아래에서는 각 메서드의 의미를 살펴보고 다른 메서드와 비교합니다. 각 메서드를 완전히 이해하려면 다양한 메서드를 읽어 보세요. 프로덕션 데이터베이스 시스템을 포함하지 않는 테스트를 작성하려는 경우, 데이터 레이어의 포괄적이고 안정적인 스텁/모의를 허용하는 유일한 접근 방식은 리포지토리 레이어입니다. 그러나 이러한 접근 방식은 구현 및 유지 관리 측면에서 상당한 비용이 듭니다.

모조 데이터베이스로서의 SQLite

가능한 테스트 접근 방식 중 하나는 프로덕션 데이터베이스(예: SQL Server)를 SQLite로 교환하여 효과적으로 테스트 ‘모조’로 사용하는 것입니다. SQLite에는 손쉬운 설정 외에도 테스트에 특히 유용한 메모리 내 데이터베이스 기능이 있습니다. 각 테스트는 자체 메모리 내 데이터베이스에 자연스럽게 격리되며 실제 파일을 관리할 필요가 없습니다.

그러나 이 작업을 수행하기 전에 EF Core에서 다양한 데이터베이스 공급자가 각자 다르게 동작한다는 것을 숙지해야 합니다. EF Core는 기본 데이터베이스 시스템의 모든 측면을 추상화하려고 시도하지 않습니다. 즉, 기본적으로 SQLite에 대한 테스트는 SQL Server 또는 다른 데이터베이스와 동일한 결과를 보장하지 않습니다. 동작 간에 다음과 같은 차이점이 있을 수 있습니다.

  • 동일한 LINQ 쿼리가 다양한 공급자에서 각기 다른 결과를 반환할 수 있습니다. 예를 들어 SQL Server는 기본적으로 대/소문자를 구분하지 않는 문자열 비교를 수행하는 반면 SQLite는 대/소문자를 구분합니다. 이렇게 하면 SQL Server에 대해 실패할 수 있는 테스트가 SQLite에 대해서는 통과할 수 있으며 그 반대의 경우도 마찬가지입니다.
  • SQL Server에서 작동하는 일부 쿼리는 SQLite에서 지원되지 않습니다. 두 데이터베이스의 정확한 SQL 지원이 다르기 때문입니다.
  • 쿼리가 SQL Server의 EF.Functions.DateDiffDay와 같은 공급자별 메서드를 사용하는 경우 해당 쿼리는 SQLite에서 실패하며 테스트할 수 없습니다.
  • 수행 중인 작업에 따라 원시 SQL이 작동할 수도 있고, 실패하거나 다른 결과를 반환할 수도 있습니다. SQL 언어는 데이터베이스마다 여러 가지 면에서 다릅니다.

프로덕션 데이터베이스 시스템에 대해 테스트를 실행하는 것에 비해 SQLite를 시작하는 것이 비교적 간단하며 많은 사용자가 그렇게 합니다. 안타깝지만 위의 제한 사항이 처음에는 중요하지 않아 보여도 EF Core 애플리케이션을 테스트할 때 결국 문제가 되는 경향이 있습니다. 따라서 실제 데이터베이스에 대해 테스트를 작성하거나 테스트 더블을 사용하는 것이 절대적으로 필요한 경우, 아래에서 설명한 대로 리포지토리 패턴의 비용을 온보딩하는 것이 좋습니다.

테스트에 SQLite를 사용하는 방법에 대한 자세한 내용은 이 섹션을 참조하세요.

모조 데이터베이스로서의 메모리 내

SQLite의 대안으로 EF Core에는 메모리 내 공급자도 함께 제공됩니다. 이 공급자는 원래 EF Core 자체의 내부 테스트를 지원하도록 설계되었지만 일부 개발자는 EF Core 애플리케이션을 테스트할 때 이를 모조 데이터베이스로 사용합니다. 이런 방식은 최대한 피해야 합니다. 모조 데이터베이스로서의 메모리 내에는 SQLite와 동일한 문제가 있지만(위 참조) 다음과 같은 추가 제한 사항이 있습니다.

  • 일반적으로 메모리 내 공급자는 관계형 데이터베이스가 아니므로 SQLite 공급자에 비해 지원하는 쿼리 형식이 적습니다. 프로덕션 데이터베이스에 비해 더 많은 쿼리가 실패하거나 다르게 동작합니다.
  • 트랜잭션이 지원되지 않습니다.
  • 원시 SQL은 전혀 지원되지 않습니다. SQL이 SQLite와 프로덕션 데이터베이스에서 동일한 방식으로 작동하는 동안 원시 SQL을 사용할 수 있는 SQLite와 비교해 보세요.
  • 메모리 내 공급자는 성능에 최적화되지 않았으며 일반적으로 메모리 내 모드 또는 프로덕션 데이터베이스 시스템에서 SQLite보다 느리게 작동합니다.

간단히 말해서 메모리 내에는 SQLite가 가진 모든 단점 외에도 몇 가지 단점이 더 있으며 대신 제공되는 이점은 없습니다. 단순한 메모리 내 모조 데이터베이스를 찾는다면 메모리 내 공급자 대신 SQLite를 사용하세요. 하지만 아래에 설명된 대로 리포지토리 패턴을 사용하는 것을 고려해 보세요.

테스트를 위해 메모리 내를 사용하는 방법에 대한 자세한 내용은 이 섹션을 참조하세요.

DbContext 및 DbSet 모의 또는 스텁

이 접근 방식은 일반적으로 모의 프레임워크를 사용하여 DbContextDbSet의 테스트 더블을 만들고 해당 더블에 대해 테스트합니다. DbContext 모의는 Add 또는 SaveChanges() 호출과 같은 다양한 비쿼리 기능을 테스트하기에 좋은 접근 방식이 될 수 있으며 이를 통해 쓰기 시나리오에서 코드가 해당 기능을 호출했는지 확인할 수 있습니다.

그러나 쿼리가 IQueryable을 통한 정적 메서드 호출인 LINQ 연산자를 통해 표현되므로 DbSet쿼리 기능을 적절하게 모의할 수 없습니다. 따라서 일부 사용자가 ‘DbSet 모의’에 대해 이야기할 때 실제로 의미하는 것은 메모리 내 컬렉션에서 지원되는 DbSet을 만든 다음, 간단한 IEnumerable처럼 메모리 내 해당 컬렉션에 대해 쿼리 연산자를 평가한다는 것입니다. 모의라기 보다 실제로는 메모리 내 컬렉션이 실제 데이터베이스를 대체하는 일종의 모조입니다.

DbSet 자체만 모조이고 쿼리는 메모리 내로 평가되므로 이 접근 방식은 EF Core 메모리 내 공급자를 사용하는 것과 매우 유사합니다. 두 기술 모두 메모리 내 컬렉션을 통해 .NET에서 쿼리 연산자를 실행합니다. 결과적으로 이 기술에도 동일한 단점이 있습니다. 쿼리가 다르게 동작(예: 대/소문자 구분 관련)하거나 단순히 실패(예: 공급자별 메서드로 인해)하며, 원시 SQL은 작동하지 않고, 트랜잭션은 무시되는 것이 최상의 결과입니다. 따라서 이 기술은 일반적으로 쿼리 코드를 테스트하지 않도록 해야 합니다.

리포지토리 패턴

위의 접근 방식에서는 EF Core의 프로덕션 데이터베이스 공급자를 모조 테스트 공급자와 교환하거나 메모리 내 컬렉션에서 지원되는 만들려고 DbSet을 만들려고 시도했습니다. 이러한 기술은 SQLite 또는 메모리 내 프로그램의 LINQ 쿼리를 계속 평가한다는 점에서는 유사한데 궁극적으로 이로 인해 위에서 설명한 어려움이 발생합니다. 특정 프로덕션 데이터베이스에 대해 실행하도록 설계된 쿼리는 다른 곳에서는 문제 없이 안정적으로 실행할 수 없습니다.

적절하고 안정적인 테스트 더블을 위해서는 애플리케이션 코드와 EF Core 사이를 중재하는 리포지토리 레이어 도입을 고려해 보세요. 리포지토리의 프로덕션 구현은 실제 LINQ 쿼리를 포함하고 EF Core를 통해 실행합니다. 실제 LINQ 쿼리가 없어도 테스트에서 리포지토리 추상화를 직접 스텁하거나 모의하여 테스트 스택에서 EF Core를 효과적으로 제거하고 테스트가 애플리케이션 코드에만 집중하도록 할 수 있습니다.

다음 다이어그램은 모조 데이터베이스 접근 방식(SQLite/메모리 내)을 리포지토리 패턴과 비교합니다.

Comparison of fake provider with repository pattern

LINQ 쿼리는 더 이상 테스트에 포함되지 않으므로 애플리케이션에 쿼리 결과를 직접 제공할 수 있습니다. 다르게 말하면 이전 접근 방식에서는 쿼리 입력을 대략적으로 스텁할 수 있지만(예: SQL Server 테이블을 메모리 내 테이블로 대체) 실제 쿼리 연산자는 계속 메모리 내에서 실행합니다. 반면 리포지토리 패턴을 사용하면 쿼리 출력을 직접 스텁하여 훨씬 더 강력하고 집중적인 단위 테스트를 수행할 수 있습니다. 이 방법을 효과적으로 사용하려면 리포지토리에서 다시 스텁될 수 없는 IQueryable 반환 메서드를 노출해서는 안 되고 IEnumerable을 반환해야 합니다.

그러나 리포지토리 패턴에는 IEnumerable 반환 메서드의 모든 (테스트 가능한) LINQ 쿼리를 캡슐화해야 하므로 애플리케이션에 추가 아키텍처 레이어가 적용되며 구현 및 유지 관리에 상당한 비용이 발생할 수 있습니다. 특히 리포지토리에서 노출되는 쿼리에 실제 데이터베이스에 대한 테스트가 계속 필요할 수 있다는 점을 감안하면 애플리케이션을 테스트하는 방법을 선택할 때 이 비용을 무시해서는 안 됩니다.

리포지토리는 테스트 외에도 장점이 있다는 사실에 주목할 필요가 있습니다. 이를 통해 모든 데이터 액세스 코드가 애플리케이션 전체로 분산되지 않고 한 곳에 집중될 수 있으며, 애플리케이션이 여러 데이터베이스를 지원해야 하는 경우 리포지토리 추상화가 공급자 간에 쿼리를 조정하는 데 매우 유용할 수 있습니다.

리포지토리를 사용한 테스트를 보여 주는 샘플은 이 섹션을 참조하세요.

전체 비교

다음 표는 다양한 테스트 기술에 대한 빠른 비교 보기를 제공하고 각 접근 방식에서 테스트할 수 있는 기능을 보여 줍니다.

기능 메모리 내 SQLite 메모리 내 DbContext 모의 리포지토리 패턴 데이터베이스에 대한 테스트
테스트 더블 형식 모조 모조 모조 모의/스텁 진짜, 더블 아님
원시 SQL 아니요 개체
트랜잭션 아니요(무시됨)
공급자별 변환 아니요 없음 아니요
정확한 쿼리 동작 개체 개체 개체
애플리케이션의 어디에서나 LINQ 사용 아니요*

* 테스트 가능한 모든 데이터베이스 LINQ 쿼리가 스텁/모의되려면 IEnumerable 반환 리포지토리 메서드에 캡슐화되어야 합니다.

요약

  • 개발자는 실제 프로덕션 데이터베이스 시스템을 대상으로 실행되는 애플리케이션에 대해 적절한 테스트 범위를 지정하는 것이 좋습니다. 이렇게 하면 애플리케이션이 실제로 프로덕션 환경에서 작동하고 적절한 설계를 통해 테스트를 안정적이고 신속하게 실행할 수 있다는 확신을 얻을 수 있습니다. 이러한 테스트는 어떤 경우에도 필요하므로 바로 시작하는 것이 좋으며 필요에 따라 나중에 테스트 더블을 사용하여 테스트를 추가해도 좋습니다.
  • 테스트 더블을 사용하기로 했다면 모조 EF Core 공급자(Sqlite/메모리 내)를 사용하거나 DbSet을 모의하는 것보다는 EF Core 위의 데이터 액세스 레이어를 스텁 또는 모의할 수 있는 리포지토리 패턴을 구현해 보세요.
  • 특정 이유로 리포지토리 패턴 옵션을 실행할 수 없게 된 경우 SQLite 메모리 내 데이터베이스를 사용하는 것이 좋습니다.
  • 테스트 목적으로 메모리 내 공급자를 사용하지 마세요. 이는 권장되지 않으며 레거시 애플리케이션에 대해서만 지원됩니다.
  • 쿼리 목적으로 DbSet을 모의하지 마세요.