C# 人们是如何使用EntityFramework6进行单元测试的呢?

C# 人们是如何使用EntityFramework6进行单元测试的呢?,c#,entity-framework,unit-testing,entity-framework-6,C#,Entity Framework,Unit Testing,Entity Framework 6,我只是从单元测试和TDD开始。我以前涉猎过,但现在我决心将其添加到我的工作流程中,并编写更好的软件 我昨天问了一个问题,其中包括这个,但这似乎是一个问题本身。我已经坐下来开始实现一个服务类,我将使用它从控制器中抽象出业务逻辑,并使用EF6映射到特定的模型和数据交互 问题是,我已经封锁了我自己,因为我不想将EF抽象到存储库中(对于特定的查询,它仍然可以在服务之外使用,等等),并且想测试我的服务(将使用EF上下文) 我想问题是,这样做有意义吗?如果是这样的话,鉴于IQueryable造成的抽象漏洞和

我只是从单元测试和TDD开始。我以前涉猎过,但现在我决心将其添加到我的工作流程中,并编写更好的软件

我昨天问了一个问题,其中包括这个,但这似乎是一个问题本身。我已经坐下来开始实现一个服务类,我将使用它从控制器中抽象出业务逻辑,并使用EF6映射到特定的模型和数据交互

问题是,我已经封锁了我自己,因为我不想将EF抽象到存储库中(对于特定的查询,它仍然可以在服务之外使用,等等),并且想测试我的服务(将使用EF上下文)

我想问题是,这样做有意义吗?如果是这样的话,鉴于IQueryable造成的抽象漏洞和许多关于单元测试的伟大文章并不简单,因为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)
    {

    }
}
公共接口IContext
{
IDbSet产品{get;set;}
IDbSet类别{get;set;}
int SaveChanges();
}
公共类DataContext:DbContext、IContext
{
公共IDbSet产品{get;set;}
公共IDbSet类别{get;set;}
公共数据上下文(字符串连接字符串)
:基本(连接字符串)
{
}
}
服务

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;
    }
}
公共类ProductService:IPProductService
{
私有IContext_上下文;
公共产品服务(IContext dbContext)
{
_context=dbContext;
}
公共IEnumerable GetAll()
{
var query=来自_context.Products中的p
选择p;
返回查询;
}
}
目前我的心态是做几件事:

  • 用这种方法模拟EF上下文-或者直接在接口上使用模拟框架,比如moq-承受单元测试可能通过但不一定是端到端工作并用集成测试进行备份的痛苦
  • 也许是在模仿EF——我从未使用过它,也不确定是否有人在野外使用过它
  • 不必测试任何简单地调用EF的东西——那么直接调用EF的服务方法(getAll等)本质上不是经过单元测试的,而是经过集成测试的

  • 有人在没有回购协议的情况下真的这么做并取得成功吗?

    我不会对我不拥有的代码进行单元测试。你在这里测试什么,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();
        }
    
        ...
    }
    
    //这是可测试的,只需在单元测试期间插入IPProductDAO的模拟即可
    公共类ProductService:IPProductService
    {
    私有IPProductDao_productDAO;
    公共产品服务(IPProductDAO productDAO)
    {
    _productDAO=productDAO;
    }
    公共列表GetAllProducts()
    {
    return_productDAO.GetAll();
    }
    ...
    }
    

    <>我会认为现场数据访问层是集成测试的一部分,而不是单元测试。我以前见过一些人对hibernate访问数据库的次数进行验证,但他们参与的项目涉及其数据存储中的数十亿条记录,这些额外的访问非常重要。

    如果你想单元测试代码,那么你需要隔离你想测试的代码(在本例中是你的服务)来自外部资源(如数据库)。您可能可以使用某种存储库模式来实现这一点,但是更常见的方法是抽象出您的EF实现,例如使用某种存储库模式。如果没有这种隔离,您编写的任何测试都将是集成测试,而不是单元测试

    至于测试EF代码——我为我的存储库编写自动集成测试,在初始化过程中将不同的行写入数据库,然后调用我的存储库实现以确保它们的行为符合预期(例如,确保结果被正确筛选,或以正确的顺序排序)


    这些是集成测试而不是单元测试,因为测试依赖于数据库连接的存在,并且目标数据库已经安装了最新的模式。

    这是我非常感兴趣的主题。有许多纯粹主义者说你不应该测试像EF和NHibernate这样的技术。他们是对的,他们已经经过了严格的测试,正如前面的回答所说,花大量时间测试你不拥有的东西通常是毫无意义的

    但是,您确实拥有下面的数据库在我看来,这就是这种方法失败的地方,您不需要测试EF/NH是否正确地完成了他们的工作。您需要测试映射/实现是否与数据库一起工作。在我看来,这是您可以测试的系统中最重要的部分之一

    但是严格地说,我们正在从单元测试领域转移到集成测试领域,但原则保持不变

    你需要做的第一件事就是能够模仿你的DAL,这样你的BLL c
    [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);
      }
    }
    
    [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);
    }
    
    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);
      }
    }
    
    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());
          });
       }
    }
    
    Use TransactionScope in the tests methods to avoid changes in the database.
    
    [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();
            }
        }
    }
    
    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
        // ...    
      }
    }
    
    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();
      }
    }
    
    [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
      }
    
    }
    
    - MyProject
      - Features
        - MyFeature
          - Queries
          - Commands
          - Services
          - DependencyConfig.cs (Ninject feature modules)
    
    return new MyContext(@"Server=(localdb)\mssqllocaldb;Database=EFProviders.InMemory;Trusted_Connection=True;");
    
    public virtual DbSet<Branch> Branches { get; set; }
    public virtual DbSet<Warehouse> Warehouses { get; set; }
    
    internal static Db Bootstrap(bool onlyMockPassedTables = false, List<Branch> branches = null, List<Products> products = null, List<Warehouses> warehouses = null)
    {
        if (onlyMockPassedTables == false) {
            branches ??= new List<Branch> { MakeBranch() };
            warehouses ??= new List<Warehouse>{ MakeWarehouse() };
        }
    
        branches?.ForEach(b => {
            b.Warehouse = warehouses.FirstOrDefault(w => w.ID == b.WarehouseID);
        });
    
        warehouses?.ForEach(w => {
            w.Branches = branches.Where(b => b.WarehouseID == w.ID);
        });
    
         var context = new Db(new DbContextOptionsBuilder<Db>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options);
         context.Branches.AddRange(branches);
         context.Warehouses.AddRange(warehouses);
         context.SaveChanges();
         return context;
     }
    
     internal const int BranchID = 1;
     internal const int WarehouseID = 2;
    
     internal static Branch MakeBranch(int id = BranchID, string code = "The branch", int warehouseId = WarehouseID) => new Branch { ID = id, Code = code, WarehouseID = warehouseId };
     internal static Warehouse MakeWarehouse(int id = WarehouseID, string code = "B", string name = "My Big Warehouse") => new Warehouse { ID = id, Code = code, Name = name };
    
    [Test]
    [TestCase(new string [] {"ABC", "DEF"}, "ABC", ExpectedResult = 1)]
    [TestCase(new string [] {"ABC", "BCD"}, "BC", ExpectedResult = 2)]
    [TestCase(new string [] {"ABC"}, "EF", ExpectedResult = 0)]
    [TestCase(new string[] { "ABC", "DEF" }, "abc", ExpectedResult = 1)]
    public int Given_SearchingForBranchByName_Then_ReturnCount(string[] codesInDatabase, string searchString)
    {
        // Arrange
        var branches = codesInDatabase.Select(x => UnitTestHelpers.MakeBranch(code: $"qqqq{x}qqq")).ToList();
        var db = UnitTestHelpers.Bootstrap(branches: branches);
        var service = new BranchService(db);
    
        // Act
        var result = service.SearchByName(searchString);
    
        // Assert
        return result.Count();
    }