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

방금 단위 테스트와 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 없이이 작업을 수행하고 성공한 사람이 있습니까?



답변

이것은 제가 관심이있는 주제입니다. 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());
      });
   }
}

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


답변

노력 경험 피드백 여기

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

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

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

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

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


설명을 위해 편집 :

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

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

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


답변

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

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

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


답변

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

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

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

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

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

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


답변

내가 소유하지 않은 단위 테스트 코드는 없습니다. 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- 내 응용 프로그램이 데이터베이스에 액세스하는 경우 왜 테스트하지 않아야합니까? 데이터 액세스에 문제가 있으면 어떻게합니까? 테스트는 미리 알고 문제에 대해 경고해야합니다.

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();
        }
    }
}


답변

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