Entity Framework 6을 사용하여 인력 단위 테스트를 어떻게 수행해야합니까?


170

방금 단위 테스트와 TDD로 시작했습니다. 나는 전에 덤벼 들었지만 지금은 그것을 워크 플로에 추가하고 더 나은 소프트웨어를 작성하기로 결정했습니다.

나는 어제 이런 종류의 질문을 물었지만 그 자체로는 질문 인 것 같습니다. 컨트롤러에서 비즈니스 로직을 추상화하고 EF6을 사용하여 특정 모델 및 데이터 상호 작용에 매핑하는 데 사용할 서비스 클래스 구현을 시작했습니다.

문제는 저장소에서 EF를 추상화하고 싶지 않기 때문에 이미로드 블록을 사용하고 있다는 것입니다 (특정 쿼리 등을 위해 서비스 외부에서 계속 사용할 수 있음). 내 서비스를 테스트하고 싶습니다 (EF 컨텍스트가 사용됨) .

여기에 질문이 있다고 생각합니다.이 점이 있습니까? 그렇다면, 사람들은 IQueryable에 의한 누수 추상화와 Ladislav Mrnka 의 많은 위대한 게시물에 비추어 어떻게 메모리에서 작업 할 때 Linq 공급자의 차이로 인해 단위 테스트의 주제가 간단하지 않은지 에 대해 어떻게합니까? 특정 데이터베이스에 적용되는 구현.

테스트하려는 코드는 매우 간단합니다. (이것은 내가하고있는 일을 이해하고 시도하는 더미 코드 일뿐입니다 .TDD를 사용하여 창조를 추진하고 싶습니다)

문맥

public interface IContext
{
    IDbSet<Product> Products { get; set; }
    IDbSet<Category> Categories { get; set; }
    int SaveChanges();
}

public class DataContext : DbContext, IContext
{
    public IDbSet<Product> Products { get; set; }
    public IDbSet<Category> Categories { get; set; }

    public DataContext(string connectionString)
                : base(connectionString)
    {

    }
}

서비스

public class ProductService : IProductService
{
    private IContext _context;

    public ProductService(IContext dbContext)
    {
        _context = dbContext;
    }

    public IEnumerable<Product> GetAll()
    {
        var query = from p in _context.Products
                    select p;

        return query;
    }
}

현재 몇 가지 일을하고 있습니다.

  1. 다음과 같은 접근 방식으로 EF 컨텍스트 조롱-단위 테스트 또는 moq와 같은 인터페이스에서 조롱 프레임 워크를 직접 사용할 때 Mocking EF- 단위 테스트가 통과 할 수 있지만 반드시 끝까지 작동하지 않아도 통합 테스트로 다시 작동하는 고통을 겪고 있습니까?
  2. 어쩌면 EF를 조롱하기 위해 노력 하는 것과 같은 것을 사용했을 수도 있습니다-나는 그것을 사용한 적이 없으며 다른 사람이 야생에서 그것을 사용하고 있는지 확실하지 않습니까?
  3. 단순히 EF를 호출하는 것을 테스트하지 않아도됩니다. 따라서 EF를 직접 호출하는 서비스 메소드 (getAll 등)는 단위 테스트가 아니라 통합 테스트 만됩니까?

실제로 Repo 없이이 작업을 수행하고 성공한 사람이 있습니까?


이봐 Modika, 나는 (때문에이 질문의 : 최근에 이것에 대해 생각하고 있었다 stackoverflow.com/questions/25977388/... ) 나는 순간에 더 공식적으로 내가 어떻게 작동하는지 조금 설명하려고 그것에서는, 그러나 나는 방법을 듣고 싶네요 당신은 그것을하고 있습니다.
samy

안녕 @ 새미, 우리가하기로 결정한 방식은 EF에 직접 닿은 것을 단위 테스트하지 않았습니다. 쿼리는 단위 테스트가 아닌 통합 테스트로 테스트되었습니다. EF를 조롱하는 것은 조금 더럽게 느껴지지만이 프로젝트는 규모가 작기 때문에 데이터베이스에 부딪힌 많은 테스트가 성능에 미치는 영향은 그리 중요하지 않았으므로 우리는 그것에 대해 좀 더 실용적이 될 수있었습니다. 나는 여전히 최선의 접근 방식이 당신에게 완전히 진실하다는 것을 100 % 확신하지 못한다. 어느 시점에서 EF (그리고 당신의 DB)를 칠 것이고 단위 테스트는 나에게 맞지 않는다.
Modika

답변:


186

이것은 제가 관심이있는 주제입니다. EF와 NHibernate와 같은 기술을 테스트해서는 안된다고 말하는 많은 순수 주의자들이 있습니다. 그들은 옳고 이미 엄격하게 테스트를 거쳤으며 이전 답변에서 언급했듯이 소유하지 않은 것을 테스트하는 데 많은 시간을 소비하는 것이 무의미합니다.

그러나 아래 데이터베이스를 소유하고 있습니다! 이것은 내 의견으로는이 접근법이 세분화 된 곳이므로 EF / NH가 올바르게 작업하고 있는지 테스트 할 필요가 없습니다. 맵핑 / 구현이 데이터베이스에서 작동하는지 테스트해야합니다. 제 생각에는 이것이 테스트 할 수있는 시스템의 가장 중요한 부분 중 하나입니다.

엄밀히 말하면 우리는 단위 테스트 영역에서 통합 테스트 영역으로 이동하고 있지만 원칙은 동일합니다.

가장 먼저해야 할 일은 DAL을 조롱하여 BLL을 EF 및 SQL과 독립적으로 테스트 할 수 있도록하는 것입니다. 이것들은 단위 테스트입니다. 다음으로 DAL을 증명하기 위해 통합 테스트 를 설계해야합니다 .

고려해야 할 몇 가지 사항이 있습니다.

  1. 데이터베이스는 각 테스트마다 알려진 상태에 있어야합니다. 대부분의 시스템은 백업을 사용하거나이를위한 스크립트를 만듭니다.
  2. 각 테스트는 반복 가능해야합니다
  3. 각 테스트는 원 자성이어야합니다

데이터베이스 설정에는 두 가지 주요 접근 방식이 있으며, 첫 번째는 UnitTest create DB 스크립트를 실행하는 것입니다. 이렇게하면 단위 테스트 데이터베이스가 각 테스트 시작시 항상 동일한 상태에있게됩니다 (이를 보장하기 위해이를 재설정하거나 트랜잭션에서 각 테스트를 실행할 수 있음).

다른 옵션은 내가하는 일입니다. 각 개별 테스트마다 특정 설정을 실행하십시오. 나는 이것이 두 가지 주요 이유로 가장 좋은 방법이라고 생각합니다.

  • 데이터베이스가 더 간단합니다. 각 테스트마다 전체 스키마가 필요하지 않습니다.
  • 각 테스트는 더 안전합니다. 작성 스크립트에서 하나의 값을 변경해도 수십 개의 다른 테스트는 무효화되지 않습니다.

불행히도 여기서 타협은 속도입니다. 이러한 모든 테스트 / 실행 스크립트를 실행하려면 시간이 걸립니다.

마지막으로 ORM을 테스트하기 위해 많은 양의 SQL을 작성하는 것은 매우 어려운 일입니다. 이것은 내가 매우 불쾌한 접근법을 취하는 곳입니다 (여기서 순수 주의자들은 저와 동의하지 않을 것입니다). ORM을 사용하여 테스트를 작성합니다! 내 시스템의 모든 DAL 테스트에 대해 별도의 스크립트를 사용하는 대신 객체를 생성하고 컨텍스트에 연결하여 저장하는 테스트 설정 단계가 있습니다. 그런 다음 테스트를 실행합니다.

이것은 이상적인 솔루션과는 거리가 멀지 만 실제로는 관리하기가 훨씬 쉽고 (특히 수천 번의 테스트가있을 때) 그렇지 않으면 대량의 스크립트를 작성합니다. 순도에 대한 실용성.

나는 몇 년 (수개월 / 일) 후에이 답을 되돌아보고 나의 접근 방식이 변함에 따라 나 자신의 의견에 동의하지 않을 것이다. 그러나 이것이 나의 현재 접근 방법이다.

위에서 말한 모든 것을 시도하고 요약하면 일반적인 DB 통합 테스트입니다.

[Test]
public void LoadUser()
{
  this.RunTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    return user.UserID;
  }, id => // the ID of the entity we need to load
  {
     var user = LoadMyUser(id); // load the entity
     Assert.AreEqual("Mr", user.Title); // test your properties
     Assert.AreEqual("Joe", user.Firstname);
     Assert.AreEqual("Bloggs", user.Lastname);
  }
}

여기서 주목해야 할 것은 두 루프의 세션이 완전히 독립적이라는 것입니다. RunTest를 구현할 때 컨텍스트가 커밋되고 삭제되고 데이터가 두 번째 부분의 데이터베이스에서만 나올 수 있는지 확인해야합니다.

2014 년 10 월 13 일 수정

나는 아마 앞으로 몇 달 동안이 모델을 개정 할 것이라고 말했다. 위에서 주장한 접근 방식에 크게 의존하는 동안 테스트 메커니즘을 약간 업데이트했습니다. 나는 이제 TestSetup 및 TestTearDown에서 엔티티를 만드는 경향이 있습니다.

[SetUp]
public void Setup()
{
  this.SetupTest(session => // the NH/EF session to attach the objects to
  {
    var user = new UserAccount("Mr", "Joe", "Bloggs");
    session.Save(user);
    this.UserID =  user.UserID;
  });
}

[TearDown]
public void TearDown()
{
   this.TearDownDatabase();
}

그런 다음 각 속성을 개별적으로 테스트하십시오.

[Test]
public void TestTitle()
{
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Mr", user.Title);
}

[Test]
public void TestFirstname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Joe", user.Firstname);
}

[Test]
public void TestLastname()
{
     var user = LoadMyUser(this.UserID);
     Assert.AreEqual("Bloggs", user.Lastname);
}

이 방법에는 몇 가지 이유가 있습니다.

  • 추가 데이터베이스 호출이 없습니다 (하나의 설정, 하나의 분해)
  • 테스트는 훨씬 세분화되며 각 테스트는 하나의 속성을 확인합니다
  • 테스트 방법 자체에서 Setup / TearDown 논리가 제거되었습니다.

나는 이것이 테스트 클래스를 더 간단하게 만들고 테스트를 더 세밀하게 만든다고 생각 한다 ( 단일 주장은 좋다 )

2015 년 5 월 3 일 수정

이 접근법에 대한 또 다른 개정. 클래스 레벨 설정은 속성로드와 같은 테스트에 매우 유용하지만 다른 설정이 필요한 경우 유용하지 않습니다. 이 경우 각 사례에 대해 새 클래스를 설정하는 것은 과잉입니다.

이 I에 도움이 이제 두 개의 기본 클래스를 갖는 경향이 SetupPerTestSingleSetup. 이 두 클래스는 필요에 따라 프레임 워크를 노출합니다.

에서 SingleSetup처음 편집에 설명 된대로 우리는 매우 유사한 메커니즘을 가지고있다. 예를 들면

public TestProperties : SingleSetup
{
  public int UserID {get;set;}

  public override DoSetup(ISession session)
  {
    var user = new User("Joe", "Bloggs");
    session.Save(user);
    this.UserID = user.UserID;
  }

  [Test]
  public void TestLastname()
  {
     var user = LoadMyUser(this.UserID); // load the entity
     Assert.AreEqual("Bloggs", user.Lastname);
  }

  [Test]
  public void TestFirstname()
  {
       var user = LoadMyUser(this.UserID);
       Assert.AreEqual("Joe", user.Firstname);
  }
}

그러나 올바른 엔터티 만로드되도록하는 참조는 SetupPerTest 방식을 사용할 수 있습니다.

public TestProperties : SetupPerTest
{
   [Test]
   public void EnsureCorrectReferenceIsLoaded()
   {
      int friendID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriend();
         session.Save(user);
         friendID = user.Friends.Single().FriendID;
      } () =>
      {
         var user = GetUser();
         Assert.AreEqual(friendID, user.Friends.Single().FriendID);
      });
   }
   [Test]
   public void EnsureOnlyCorrectFriendsAreLoaded()
   {
      int userID = 0;
      this.RunTest(session =>
      {
         var user = CreateUserWithFriends(2);
         var user2 = CreateUserWithFriends(5);
         session.Save(user);
         session.Save(user2);
         userID = user.UserID;
      } () =>
      {
         var user = GetUser(userID);
         Assert.AreEqual(2, user.Friends.Count());
      });
   }
}

요약하면 두 방법 모두 테스트하려는 대상에 따라 작동합니다.


2
여기 통합 테스트에 대한 다른 접근 방식. TL; DR-애플리케이션 자체를 사용하여 테스트 데이터를 설정하고 테스트마다 트랜잭션을 롤백하십시오.
Gert Arnold

3
@Liath, 훌륭한 응답. EF 테스트에 대한 의심을 확인했습니다. 내 질문은 이것입니다; 귀하의 예는 매우 구체적인 경우에 대한 것입니다. 그러나 언급했듯이 수백 개의 엔터티를 테스트해야 할 수도 있습니다. DRY 원칙 (반복하지 마십시오)에 따라 매번 동일한 기본 코드 패턴을 반복하지 않고 솔루션을 어떻게 확장합니까?
Jeffrey A. Gochin

4
문제를 완전히 회피하기 때문에 이에 동의하지 않아야합니다. 단위 테스트는 함수의 논리를 테스트하는 것입니다. OP 예에서 논리는 데이터 저장소에 종속됩니다. EF를 테스트하지 않겠다고 말했을 때 맞습니다. 그러나 그것은 문제가 아닙니다. 문제는 코드를 데이터 저장소와 별도로 테스트하는 것입니다. 매핑 테스트는 완전히 다른 주제 imo입니다. 로직이 데이터와 올바르게 상호 작용하는지 테스트하려면 상점을 제어 할 수 있어야합니다.
Sinaesthetic

7
Entity Framework 자체를 단위 테스트해야하는지 여부에 대해서는 아무도 모릅니다. 어떤 일을하고 데이터베이스에 대해 EF 호출을 수행하는 메소드를 테스트해야합니다. 목표는 빌드 서버에서 데이터베이스를 요구하지 않고이 방법을 테스트 할 수 있도록 EF를 모의하는 것입니다.
머핀 맨

4
나는 여행을 정말 좋아합니다. 시간이 지남에 따라 수정 사항을 추가해 주셔서 감사합니다. 소스 제어를 읽고 생각이 어떻게 발전했는지 이해하는 것과 같습니다. 나는 기능적 (EF 포함)과 단위 (모의 EF) 구분도 정말 감사합니다.
Tom Leys

21

노력 경험 피드백 여기

많은 독서 끝에 나는 테스트에서 노력 을 사용하고 있습니다 : 테스트하는 동안 컨텍스트는 메모리 버전을 반환하는 팩토리에 의해 빌드되므로 매번 빈 슬레이트에 대해 테스트 할 수 있습니다. 테스트 외부에서 팩토리는 전체 컨텍스트를 리턴하는 팩토리로 분석됩니다.

그러나 데이터베이스의 모든 기능을 갖춘 모의에 대한 테스트가 테스트를 끌어 내리는 경향이 있다고 생각합니다. 시스템의 한 부분을 테스트하려면 전체 종속성을 설정해야합니다. 또한 모든 것을 처리하는 거대한 객체가 하나뿐이기 때문에 관련이없는 테스트를 함께 구성하는 경향이 있습니다. 주의를 기울이지 않으면 단위 테스트 대신 통합 테스트를 수행 할 수 있습니다.

나는 거대한 DBContext보다는 더 추상적 인 것에 대한 테스트를 선호했지만 의미있는 테스트와 베어 본 테스트 사이의 달콤한 지점을 찾을 수 없었습니다. 나의 경험이없는 것으로 그것을 분필로 치십시오.

그래서 노력이 흥미로워 요. 당신이지면을 칠 필요가 있다면 그것은 빨리 시작하고 결과를 얻는 좋은 도구입니다. 그러나 나는 조금 더 우아하고 추상적 인 것이 다음 단계가되어야한다고 생각합니다. 그리고 이것이 제가 다음에 조사 할 것입니다. 이 게시물을 좋아하여 다음 위치로 이동 :)

추가 편집 : 워밍업에 시간이 걸리므로 대략적으로 살펴보십시오. 테스트 시작시 5 초 테스트 스위트를 매우 효율적으로 사용하려면 문제가 될 수 있습니다.


설명을 위해 편집 :

나는 노력을 사용하여 웹 서비스 앱을 테스트했습니다. 입력되는 각 메시지 M은 IHandlerOf<M>비아 윈저로 라우팅됩니다 . Castle.Windsor IHandlerOf<M>는 구성 요소의 종속성을 해결하는 문제를 해결합니다 . 이러한 종속성 중 하나는 DataContextFactory처리기에서 팩토리를 요청할 수있게하는입니다.

내 테스트에서 IHandlerOf 구성 요소를 직접 인스턴스화하고 SUT의 모든 하위 구성 요소를 조롱 DataContextFactory하고 처리기에 래핑 된 Effort를 처리 합니다.

DB가 내 테스트에 맞았 기 때문에 엄격한 의미에서 단위 테스트를하지 않습니다. 그러나 위에서 말했듯이지면을 치고 응용 프로그램의 일부 포인트를 신속하게 테스트 할 수 있습니다.


입력에 감사드립니다. Bonafide 지불 작업 이므로이 프로젝트를 실행해야하기 때문에 수행 할 수있는 작업은 일부 repos로 시작하여 어떻게 진행되는지 확인하지만 노력은 매우 흥미 롭습니다. 응용 프로그램에서 어떤 계층을 사용하고 계십니까?
Modika

2
노력이 제대로 거래를 지원 한 경우에만
Sedat Kapanoglu

문자열에 null 대신 ''를 사용하면 csv 로더가있는 문자열에 노력이 있습니다.
Sam

13

당신이 원하는 경우 단위 테스트 코드 당신은 외부 자원에서 (이 경우 서비스에) (예 : 데이터베이스) 시험에 원하는 코드를 분리해야합니다. 아마도 일종의 인 메모리 EF 공급자를 사용 하여이 작업을 수행 할 수 있지만 훨씬 더 일반적인 방법은 EF 구현을 추상화하는 것입니다 (예 : 일종의 저장소 패턴). 이러한 격리가 없으면 작성하는 테스트는 단위 테스트가 아닌 통합 테스트가됩니다.

EF 코드 테스트와 관련하여-초기화 중에 데이터베이스에 다양한 행을 쓰는 저장소에 대한 자동화 된 통합 테스트를 작성한 다음 저장소 구현을 호출하여 예상대로 작동하는지 확인하십시오 (예 : 결과가 올바르게 필터링되는지 확인). 올바른 순서로 정렬되어 있습니다).

테스트에는 데이터베이스 연결이 있어야하고 대상 데이터베이스에 최신 스키마가 이미 설치되어 있으므로 단위 테스트가 아닌 통합 테스트입니다.


고마워 @ 저스틴 리포지토리 패턴에 대해 알고 있지만 ayende.com/blog/4784/…lostechies.com/jimmybogard/2009/09/11/wither-the-repository 와 같은 것들을 읽는 것은 내가 돈이라고 생각하게했습니다. 이 추상화 계층을 원하지 않지만 다시 한 번 쿼리 접근 방식에 대해 더 많이 이야기하며 매우 혼란 스럽습니다.
Modika

7
@Modika Ayende는 비판하기 위해 저장소 패턴의 구현을 잘못 선택했으며 결과적으로 100 %가 맞습니다. 오버 엔지니어링되었으며 어떠한 이점도 제공하지 않습니다. 올바른 구현은 코드의 단위 테스트 가능 부분을 DAL 구현과 분리합니다. NHibernate와 EF를 사용하면 코드를 직접 테스트하기가 어렵고 (불가능하지는 않지만) 견고한 모 놀리 식 코드베이스가됩니다. 나는 여전히 저장소 패턴에 대해 다소 회의적이지만, DAL 구현을 어떻게 든 분리해야하고 저장소가 지금까지 찾은 최고의 것임을 100 % 확신합니다.
Justin

2
@Modika 두 번째 기사를 다시 읽으십시오. "나는이 추상화 계층을 원하지 않는다"고 그가 말하는 것은 아니다. 또한 Fowler ( martinfowler.com/eaaCatalog/repository.html ) 또는 DDD ( dddcommunity.org/resources/ddd_terms ) 에서 원래 리포지토리 패턴에 대해 읽으십시오 . 원래의 개념을 완전히 이해하지 않고서도 전문가들을 믿지 마십시오. 그들이 실제로 비판하는 것은 패턴 자체가 아니라 최근에 패턴을 잘못 사용한 것입니다 (아마도 이것을 알지 못하지만).
guillaume31

1
@ guillaume31 나는 저장소 패턴에 반하지 않다 (나는 그것을 이해한다) 나는 그 수준에서 이미 추상화 된 것을 추상화하기 위해 필요한지 알아 내려고 노력하고 있으며, 그것을 모방하여 EF에 대해 직접 테스트하고 그것을 생략 할 수 있다면 내 응용 프로그램에서 더 높은 계층의 테스트에서 사용하십시오. 또한 repo를 사용하지 않으면 EF 확장 기능 세트의 이점을 얻을 수 있으며 repo를 사용하면 얻을 수 없습니다.
Modika

저장소와 함께 DAL을 분리 한 후에는 데이터베이스 (EF)를 "모의"해야합니다. 지금까지 컨텍스트와 다양한 비동기 확장 (ToListAsync (), FirstOrDefaultAsync () 등)을 조롱하면 좌절감을 느꼈습니다.
케빈 버튼

9

Entity Framework는 구현 방식이므로 데이터베이스 상호 작용의 복잡성을 추상화하고 직접 상호 작용하는 것은 여전히 ​​긴밀한 결합이므로 테스트하기가 혼란 스럽습니다.

단위 테스트는 함수의 논리와 모든 잠재적 결과를 외부 종속성 (이 경우 데이터 저장소)과 분리하여 테스트하는 것입니다. 그러기 위해서는 데이터 저장소의 동작을 제어 할 수 있어야합니다. 예를 들어, 가져온 사용자가 일부 기준 세트를 충족하지 않으면 함수가 false를 리턴한다고 주장하려면 기준을 충족하지 못하는 사용자를 항상 리턴하도록 [mocked] 데이터 저장소를 구성해야합니다. 반대 주장의 경우도 마찬가지입니다.

그렇게 말하고 EF가 구현이라는 사실을 받아들이면 저장소를 추상화하는 아이디어를 선호 할 것입니다. 약간 중복 된 것 같습니까? 데이터 구현에서 코드를 분리하는 문제를 해결하고 있기 때문이 아닙니다.

DDD에서 리포지토리는 DAO가 아닌 집계 루트 만 반환합니다. 이런 식으로 리포지토리 소비자는 데이터 구현에 대해 알 필요가 없으며 (필수하지 않아야 함)이 문제를 해결하는 방법의 예로 사용할 수 있습니다. 이 경우 EF에 의해 생성 된 개체는 DAO이므로 응용 프로그램에서 숨겨져 야합니다. 이것은 정의한 저장소의 또 다른 이점입니다. EF 오브젝트 대신 비즈니스 오브젝트를 리턴 유형으로 정의 할 수 있습니다. 이제 repo가하는 일은 EF에 대한 호출을 숨기고 reF 서명에 정의 된 해당 비즈니스 오브젝트에 EF 응답을 맵핑하는 것입니다. 이제 클래스에 주입하는 DbContext 종속성 대신 해당 저장소를 사용할 수 있으므로 이제 해당 인터페이스를 모의하여 코드를 개별적으로 테스트하는 데 필요한 제어를 제공 할 수 있습니다.

조금 더 많은 일을하고 코를 엄지 손가락으로 치지 만 실제 문제를 해결합니다. 옵션이 될 수있는 다른 대답으로 언급 된 메모리 내 공급자가 있으며 (내가 시도하지는 않았 음) 그 존재 자체는 연습이 필요하다는 증거입니다.

코드를 격리시키는 실제 문제를 회피 한 다음 매핑 테스트에 대해 접하기 때문에 최상위 답변에 완전히 동의하지 않습니다. 원하는 경우 매핑을 테스트하지만 실제 문제를 해결하고 실제 코드 범위를 얻으십시오.


8

내가 소유하지 않은 단위 테스트 코드는 없습니다. MSFT 컴파일러가 작동하는지 여기에서 무엇을 테스트하고 있습니까?

즉,이 코드를 테스트 할 수있게하려면 데이터 액세스 계층을 비즈니스 로직 코드와 별도로 만들어야합니다. 내가하는 일은 모든 EF 물건을 가져 와서 해당 인터페이스가있는 (또는 여러) DAO 또는 DAL 클래스에 넣는 것입니다. 그런 다음 인터페이스로 참조되는 종속성 (생성자 주입)으로 DAO 또는 DAL 객체를 주입 할 서비스를 작성합니다. 이제 테스트해야하는 부분 (코드)은 DAO 인터페이스를 모의하고이를 단위 테스트 내부의 서비스 인스턴스에 주입하여 쉽게 테스트 할 수 있습니다.

//this is testable just inject a mock of IProductDAO during unit testing
public class ProductService : IProductService
{
    private IProductDAO _productDAO;

    public ProductService(IProductDAO productDAO)
    {
        _productDAO = productDAO;
    }

    public List<Product> GetAllProducts()
    {
        return _productDAO.GetAll();
    }

    ...
}

라이브 데이터 액세스 계층은 단위 테스트가 아닌 통합 테스트의 일부로 간주합니다. 나는 사람들이 이전에 최대 절전 모드로 데이터베이스를 몇 번이나 여행했는지 확인하는 것을 보았습니다.


1
답변 주셔서 감사합니다, 그러나이 수준에서 EF의 내부를 숨기고있는 저장소를 말하는 것의 차이점은 무엇입니까? 나는 여전히 IContext 인터페이스를 사용하여 EF를 추상화하고 싶지 않습니까? 나는 이것에 익숙하지 않습니다. :)
익숙하지

3
@Modika A Repo도 괜찮습니다. 원하는 패턴 "정말 EF를 추상화하고 싶지 않습니다."테스트 가능한 코드를 원하십니까?
Jonathan Henson

1
@Modika 내 요점은 문제를 분리하지 않으면 테스트 가능한 코드가 없다는 것입니다. 데이터 액세스 및 비즈니스 로직은 유지 보수가 용이 한 테스트를 수행하기 위해 별도의 계층에 있어야합니다.
Jonathan Henson

2
본질적으로 IDbSet이 저장소이고 UOW 컨텍스트이므로 저장소 추상화에서 EF를 래핑하는 것이 필요하다고 생각하지 않았습니다. 문제는 추상화와 함께 제공되며 주요 요점은 내 쿼리가 동일한 경계 (linq-to-entities vs linq-to-objects)에서 실행되지 않기 때문에 정확히 테스트하고있는 것이므로 서비스가 조금 낭비되는 것 같습니까? 아니면 여기서 잘 지내고 있습니까?
Modika

1
, 나는 당신의 일반적인 요점에 동의하지만 DbContext는 작업 단위이며 IDbSets는 저장소 구현의 일부이며, 나는 그것을 생각하는 유일한 사람이 아닙니다. EF를 모의 할 수 있으며 일부 계층에서 통합 테스트를 실행해야합니다. 리포지토리에서 또는 서비스에서 추가로 수행하는 경우 실제로 중요합니까? DB에 긴밀하게 연결되는 것은 실제로 관심사가 아니며, 그것이 일어날 것이라고 확신하지만 발생하지 않을 수있는 것을 계획하지는 않을 것입니다.
Modika

8

나는 이러한 고려 사항에 도달하기 위해 언젠가는 혼란에 빠졌다.

1- 내 응용 프로그램이 데이터베이스에 액세스하는 경우 왜 테스트하지 않아야합니까? 데이터 액세스에 문제가 있으면 어떻게합니까? 테스트는 미리 알고 문제에 대해 경고해야합니다.

2- 리포지토리 패턴은 다소 어렵고 시간이 많이 걸립니다.

그래서 나는이 접근법을 생각해 냈습니다. 나는 최선이라고 생각하지 않지만 내 기대를 충족 시켰습니다.

Use TransactionScope in the tests methods to avoid changes in the database.

이를 위해서는 다음이 필요합니다.

1- EntityFramework를 테스트 프로젝트에 설치하십시오. 2- 연결 문자열을 Test Project의 app.config 파일에 넣습니다. 3- 테스트 프로젝트에서 dll System.Transactions를 참조하십시오.

고유 한 부작용은 트랜잭션이 중단 된 경우에도 삽입하려고 할 때 ID 시드가 증가한다는 것입니다. 그러나 테스트는 개발 데이터베이스에 대해 수행되므로 문제가되지 않습니다.

샘플 코드 :

[TestClass]
public class NameValueTest
{
    [TestMethod]
    public void Edit()
    {
        NameValueController controller = new NameValueController();

        using(var ts = new TransactionScope()) {
            Assert.IsNotNull(controller.Edit(new Models.NameValue()
            {
                NameValueId = 1,
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }

    [TestMethod]
    public void Create()
    {
        NameValueController controller = new NameValueController();

        using (var ts = new TransactionScope())
        {
            Assert.IsNotNull(controller.Create(new Models.NameValue()
            {
                name1 = "1",
                name2 = "2",
                name3 = "3",
                name4 = "4"
            }));

            //no complete, automatically abort
            //ts.Complete();
        }
    }
}

1
사실, 나는이 솔루션을 많이 좋아합니다. 구현이 간단하고보다 현실적인 테스트 시나리오. 감사!
slopapa

1
EF 6에서는 DbContext.Database.BeginTransaction을 사용하지 않습니까?
SwissCoder

5

간단히 말해서, 주스는 모델 데이터를 검색하는 한 줄로 서비스 방법을 테스트하는 데 도움이되지 않습니다. 내 경험상 TDD를 처음 사용하는 사람들은 모든 것을 절대적으로 테스트하기를 원합니다. 외부 프레임 워크를 타사 프레임 워크로 추상화하는 오래된 밤은 더미 데이터를 주입 할 수 있도록 자식을 / 확장하는 프레임 워크 API를 모의 할 수 있습니다. 모든 사람이 단위 테스트가 얼마나 좋은지 다른 관점을 가지고 있습니다. 나는 요즘 더 실용적이며 내 테스트가 실제로 최종 제품에 가치를 더하는지, 그리고 어떤 비용이 드는지 스스로에게 물어 봅니다.


1
실용주의에 그렇습니다. 나는 여전히 단위 테스트의 품질이 원래 코드의 품질보다 열등하다고 주장합니다. 물론 코딩 실습을 개선하고 유지 관리 성을 향상시키기 위해 TDD를 사용하는 것이 가치가 있지만 TDD는 가치를 떨어 뜨릴 수 있습니다. 우리는 EF와 테이블 자체의 사용이 건전하다는 확신을주기 때문에 데이터베이스에 대해 모든 테스트를 실행합니다. 테스트를 실행하는 데 시간이 오래 걸리지 만 더 안정적입니다.
새비지

3

EF 기반 서비스의 단위 테스트 를 돕기 위해 현재 사용하고있는 실제 사례를 보여 드리고 간략히 논의한 접근법을 공유하고 싶습니다 .

먼저, EF Core의 메모리 내 공급자를 사용하고 싶지만 이것은 EF 6에 관한 것입니다. 또한 RavenDB와 같은 다른 스토리지 시스템의 경우 메모리 내 데이터베이스 공급자를 통한 테스트 제안자이기도합니다. 다시 말하지만, 이것은 많은 의식없이 EF 기반 코드 테스트하는 데 도움이됩니다. 됩니다.

패턴을 만들 때의 목표는 다음과 같습니다.

  • 팀의 다른 개발자가 이해하기 쉬워야합니다.
  • 가능한 가장 작은 레벨에서 EF 코드를 분리해야합니다.
  • 이상한 다중 책임 인터페이스 (예 : "일반"또는 "일반"리포지토리 패턴)를 만들면 안됩니다.
  • 단위 테스트에서 구성 및 설정이 쉬워야합니다

나는 EF가 여전히 구현 세부 사항이라는 이전 진술에 동의하며 "순수한"단위 테스트를 수행하기 위해 추상화해야한다고 생각하는 것이 좋습니다. 또한 이상적으로는 EF 코드 자체가 작동하는지 확인하고 싶지만 샌드 박스 데이터베이스, 메모리 내 공급자 등이 필요합니다. 내 접근 방식으로 두 가지 문제를 모두 해결할 수 있습니다. EF 종속 코드를 안전하게 단위 테스트 하고 만들 수 있습니다 EF 코드를 구체적으로 테스트하기위한 통합 테스트.

내가 달성 한 방법은 EF 코드 를 전용 쿼리 및 명령 클래스로 간단히 캡슐화하는 것 입니다. 아이디어는 간단합니다. 클래스에서 EF 코드를 래핑하고 원래 사용했던 클래스의 인터페이스에 의존하면됩니다. 내가 해결해야 할 주요 문제는 클래스에 많은 종속성을 추가하지 않고 테스트에서 많은 코드를 설정하는 것이 었습니다.

유용하고 간단한 라이브러리는 Mediatr 입니다. 간단한 in-process 메시징을 허용하며 코드를 구현하는 처리기에서 "요청"을 분리하여 수행합니다. 이것은 "어떻게"를 "어떻게"에서 분리시키는 이점이 있습니다. 예를 들어, EF 코드를 작은 청크로 캡슐화하면 구현을 다른 제공자 또는 완전히 다른 메커니즘으로 대체 할 수 있습니다. 조치를 수행하기 위해 요청을 보내기 만하면됩니다.

프레임 워크의 유무에 관계없이 의존성 주입을 사용하여 중재자를 쉽게 조롱하고 요청 / 응답 메커니즘을 제어하여 단위 테스트 EF 코드를 사용할 수 있습니다.

먼저 테스트해야하는 비즈니스 로직이있는 서비스가 있다고 가정 해 보겠습니다.

public class FeatureService {

  private readonly IMediator _mediator;

  public FeatureService(IMediator mediator) {
    _mediator = mediator;
  }

  public async Task ComplexBusinessLogic() {
    // retrieve relevant objects

    var results = await _mediator.Send(new GetRelevantDbObjectsQuery());
    // normally, this would have looked like...
    // var results = _myDbContext.DbObjects.Where(x => foo).ToList();

    // perform business logic
    // ...    
  }
}

이 접근법의 이점을보기 시작합니까? 모든 EF 관련 코드를 설명 클래스로 명시 적으로 캡슐화 할뿐만 아니라이 요청이 처리되는 "방법"에 대한 구현 문제를 제거하여 확장 성을 허용합니다.이 클래스는 관련 객체가 EF, MongoDB, 또는 텍스트 파일.

이제 MediatR을 통한 요청 및 처리기 :

public class GetRelevantDbObjectsQuery : IRequest<DbObject[]> {
  // no input needed for this particular request,
  // but you would simply add plain properties here if needed
}

public class GetRelevantDbObjectsEFQueryHandler : IRequestHandler<GetRelevantDbObjectsQuery, DbObject[]> {
  private readonly IDbContext _db;

  public GetRelevantDbObjectsEFQueryHandler(IDbContext db) {
    _db = db;
  }

  public DbObject[] Handle(GetRelevantDbObjectsQuery message) {
    return _db.DbObjects.Where(foo => bar).ToList();
  }
}

보다시피 추상화는 단순하고 캡슐화되어 있습니다. 또한의 절대적으로 테스트 할 통합 테스트에서, 당신이 있기 때문에 할 수 여기에 혼합 더 비즈니스 우려가없는 - 개별적으로이 클래스를 테스트합니다.

기능 서비스의 단위 테스트는 어떻게 생겼습니까? 간단합니다. 이 경우 Moq 를 사용하여 조롱합니다 (행복하게 만드는 것 사용).

[TestClass]
public class FeatureServiceTests {

  // mock of Mediator to handle request/responses
  private Mock<IMediator> _mediator;

  // subject under test
  private FeatureService _sut;

  [TestInitialize]
  public void Setup() {

    // set up Mediator mock
    _mediator = new Mock<IMediator>(MockBehavior.Strict);

    // inject mock as dependency
    _sut = new FeatureService(_mediator.Object);
  }

  [TestCleanup]
  public void Teardown() {

    // ensure we have called or expected all calls to Mediator
    _mediator.VerifyAll();
  }

  [TestMethod]
  public void ComplexBusinessLogic_Does_What_I_Expect() {
    var dbObjects = new List<DbObject>() {
      // set up any test objects
      new DbObject() { }
    };

    // arrange

    // setup Mediator to return our fake objects when it receives a message to perform our query
    // in practice, I find it better to create an extension method that encapsulates this setup here
    _mediator.Setup(x => x.Send(It.IsAny<GetRelevantDbObjectsQuery>(), default(CancellationToken)).ReturnsAsync(dbObjects.ToArray()).Callback(
    (GetRelevantDbObjectsQuery message, CancellationToken token) => {
       // using Moq Callback functionality, you can make assertions
       // on expected request being passed in
       Assert.IsNotNull(message);
    });

    // act
    _sut.ComplexBusinessLogic();

    // assertions
  }

}

필요한 것은 모두 단일 설정이며 추가 구성은 필요하지 않습니다. 매우 간단한 단위 테스트입니다. 분명히하십시오 : Mediatr (예를 들어 인터페이스를 구현하고 테스트를 위해 그것을 조롱하는 것)과 같은 일 없이 완전히 가능 IGetRelevantDbObjectsQuery하지만 실제로 많은 기능과 쿼리 / 명령이있는 큰 코드베이스의 경우 캡슐화를 좋아합니다. 선천적 DI 지원 Mediatr 오퍼.

이러한 클래스를 구성하는 방법이 궁금하다면 간단합니다.

- MyProject
  - Features
    - MyFeature
      - Queries
      - Commands
      - Services
      - DependencyConfig.cs (Ninject feature modules)

피처 슬라이스구성하는 것이 요점이지만 모든 관련 / 종속 코드를 함께 유지하고 쉽게 찾을 수 있습니다. 가장 중요한 것은 명령 / 쿼리 분리 원칙에 따라 쿼리와 명령을 분리하는 것입니다.

이것은 저의 모든 기준을 충족시킵니다. 시상식이 적고 이해하기 쉽고 숨겨진 숨겨진 혜택이 있습니다. 예를 들어, 변경 사항 저장을 어떻게 처리합니까? 이제 역할 인터페이스를 사용하여 Db 컨텍스트를 단순화 할 수 있습니다 (IUnitOfWork.SaveChangesAsync()) 및 단일 역할 인터페이스에 대한 호출을 조롱하거나 RequestHandlers 내에서 커밋 / 롤백을 캡슐화 할 수는 있지만 유지 보수가 가능한 한 원하는대로 할 수 있습니다. 예를 들어, EF 객체를 전달하고 저장 / 업데이트 / 제거 할 단일 일반 요청 / 핸들러를 만들려고했지만 의도가 무엇인지 묻고 원하는 경우 처리기를 다른 저장소 공급자 / 구현과 교체하려면 원하는 작업을 나타내는 명시적인 명령 / 쿼리를 만들어야합니다. 단일 서비스 나 기능은 특정 무언가가 필요할 때가 많으며, 필요하기 전에 일반적인 것을 만들지 마십시오.

물론 이 패턴 에는 경고가 있습니다. 간단한 펍 / 서브 메커니즘으로 너무 멀리 갈 수 있습니다. 구현을 EF 관련 코드 만 추상화하는 것으로 제한했지만, 모험을 좋아하는 개발자는 MediatR을 사용하여 모든 것을 극복하고 메시지를 작성하기 시작할 수 있습니다. 이는 MediatR의 문제가 아니라 프로세스 문제이므로이 패턴을 어떻게 사용하고 있는지 인식하십시오.

사람들이 EF를 단위 테스트 / 모의하는 방법에 대한 구체적인 예를 원했고 이것이 우리 프로젝트에서 성공적으로 작동하는 접근법이며 팀은 채택이 얼마나 쉬운 지 매우 기쁘게 생각합니다. 이게 도움이 되길 바란다! 프로그래밍의 모든 것과 마찬가지로 여러 가지 접근 방식이 있으며 모두 달성하려는 대상에 따라 다릅니다. 저는 단순성, 사용 편의성, 유지 관리 성 및 검색 가능성을 중시하며이 솔루션은 모든 요구를 충족시킵니다.


대답, 중재자를 사용하는 QueryObject 패턴에 대한 훌륭한 설명 및 프로젝트를 추진하기 시작한 것에 감사드립니다. 나는 질문을 업데이트해야 할 수도 있지만 더 이상 EF를 테스트하는 단위가 아니기 때문에 추상화가 너무 누출되어 있습니다 (SqlLite는 괜찮을 수도 있음). 그래서 데이터베이스와 단위 테스트 비즈니스 규칙 및 기타 논리를 쿼리하는 것들을 통합 테스트합니다.
Modika

3

메모리 엔터티 프레임 워크 데이터베이스 공급자 인 노력이 있습니다. 나는 실제로 그것을 시도하지 않았습니다 ... Haa는 이것이 질문에서 언급 된 것을 발견했습니다!

또는 내장 메모리 데이터베이스 공급자가있는 EntityFrameworkCore로 전환 할 수 있습니다.

https://blog.goyello.com/2016/07/14/save-time-mocking-use-your-real-entity-framework-dbcontext-in-unit-tests/

https://github.com/tamasflamich/effort

컨텍스트를 얻기 위해 팩토리를 사용했기 때문에 컨텍스트를 사용하는 데 가깝게 만들 수 있습니다. 이것은 Visual Studio에서 로컬로 작동하지만 TeamCity 빌드 서버에서는 작동하지 않는 것 같습니다. 아직 이유는 확실하지 않습니다.

return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");

앤드류 안녕, 문제는 결코 컨텍스트를 얻지 못했습니다. 컨텍스트를 추상화하고 공장에서 빌드 한 컨텍스트를 공장에서 처리 할 수 ​​있습니다. 가장 큰 문제는 메모리에 있던 것과 Linq4Entities가하는 것의 일관성이었습니다. 동일하지 않아 잘못된 테스트로 이어질 수 있습니다. 현재, 우리는 단지 테스트 데이터베이스를 통합하고 있습니다.
Modika

이 Moq 도우미는 조롱 할 문맥이 있으면 작동합니다 ( codeproject.com/Tips/1045590/… ). 목업 아웃 컨텍스트를 목록으로 백업하면 SQL 데이터베이스가 지원하는 컨텍스트처럼 작동하지 않을 수 있습니다.
앤드류 페이트

2

나는 필터를 코드의 다른 부분과 분리하고 블로그의 개요에 따라 필터를 테스트하고 싶습니다 .

즉, 테스트중인 필터 논리는 LINQ 표현식과 T-SQL과 같은 기본 쿼리 언어 간의 변환으로 인해 프로그램이 실행될 때 실행되는 필터 논리와 동일하지 않습니다. 여전히 필터의 논리를 확인할 수 있습니다. 레이어 간 통합을 테스트 할 때까지 발생하는 변환과 대 / 소문자 구분 및 null 처리와 같은 일에 대해 너무 걱정하지 않아도됩니다.


0

엔터티 프레임 워크에서 수행 할 작업을 테스트 (예 : 기대치 검증)하는 것이 중요합니다. 내가 성공적으로 사용한 방법 중 하나는이 예제에 표시된 것처럼 moq를 사용하는 것입니다 (이 답변에 오랫동안 복사하기 위해).

https://docs.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking

그러나 조심하십시오 ... linq 쿼리에 적절한 "OrderBy"가 없으면 SQL 컨텍스트가 특정 순서로 항목을 반환한다고 보장 할 수 없으므로 메모리 내 목록을 사용하여 테스트 할 때 통과하는 항목을 작성할 수 있습니다 linq-to-entities))하지만 (linq-to-sql)이 사용되면 uat / live 환경에서 실패합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.