代码之家  ›  专栏  ›  技术社区  ›  user1447679

如何在DataContext中正确使用moq ExecuteQuery方法?

  •  1
  • user1447679  · 技术社区  · 6 年前

    我很难理解如何在单元测试中正确地从模拟数据库调用返回模拟数据。

    下面是一个我想进行单元测试的示例方法( 获取建筑物 ):

    public class BuildingService : IBuildingService {
    
        public IQueryable<Building> GetBuildings(int propertyId)
        {
            IQueryable<Building> buildings;
    
            // Execution path for potential exception thrown
            // if (...) throw new SpecialException();
    
            // Another execution path...
            // if (...) ...
    
            using (var context = DataContext.Instance())
            {
                var Params = new List<SqlParameter>
                {
                    new SqlParameter("@PropertyId", propertyId)
                };
    
                // I need to return mocked data here...
                buildings = context
                  .ExecuteQuery<Building>(System.Data.CommandType.StoredProcedure, "dbo.Building_List", Params.ToArray<object>())
                  .AsQueryable();
    
    
            }
    
            return buildings;
        }
    
    }
    

    所以 获取建筑物 调用存储过程。

    所以我需要模拟DataContext,我可以覆盖它并设置一个可测试的实例。在上面的例子中 DataContext.Instance() 返回模拟对象。

    [TestFixture]
    public class BuildingServiceTests
    {
    
        private Mock<IDataContext> _mockDataContext;
    
        [SetUp]
        public void Setup() {
            _mockDataContext = new Mock<IDataContext>();
        }
    
        [TearDown]
        public void TearDown() {
            ...
        }
    
        [Test]
        public void SomeTestName() {
    
            _mockDataContext.Setup(r => 
                r.ExecuteQuery<Building>(CommandType.StoredProcedure, "someSproc"))
                .Returns(new List<Building>() { new Building() { BuildingId = 1, Title = "1" }}.AsQueryable());
    
          DataContext.SetTestableInstance(_mockDataContext.Object); 
            var builings = BuildingService.GetBuildings(1, 1);
    
          // Assert...
    
        }
    

    请忽略一些参数,如 propertyId .我把这些都删掉了,把这一切都简化了。我根本无法得到 ExecuteQuery 方法返回任何数据。

    我可以模拟的所有其他简单的peta poco类型的方法都没有问题(即Get、Insert、Delete)。

    使现代化

    DataContext.Instance 返回DataContext类的活动实例(如果存在),如果不存在,则返回新实例。因此,所讨论的测试方法返回模拟实例。

    2 回复  |  直到 6 年前
        1
  •  2
  •   Fabio    6 年前

    不要嘲笑 DataContext .因为嘲笑 数据上下文 将生成与的实现细节紧密耦合的测试 数据上下文 。并且您将被迫为代码中的每一个更改更改测试,甚至行为也将保持不变。

    相反,引入一个“DataService”接口,并在的测试中对其进行模拟 BuildingService

    public interface IDataService
    {
        IEnumerable<Building> GetBuildings(int propertyId)
    }
    

    然后,您可以测试 IDataService agains real database作为集成测试的一部分,或对内存中的数据库进行测试。

    如果您可以使用“InMemory”数据库(EF Core或Sqlite)进行测试,那就更好了->为编写测试 建筑服务 针对实际执行 数据上下文

    在测试中,您应该只模拟外部资源(web服务、文件系统或数据库),或者只模拟使测试变慢的资源。

    在重构代码库时,不模仿其他依赖项将节省您的时间并提供自由。

    更新后:

    根据更新的问题,其中 建筑服务 有一些执行路径-您仍然可以测试 建筑服务 和抽象数据相关逻辑 IDataService

    下面的示例是 建筑服务

    public class BuildingService
    {
        private readonly IDataService _dataService;
    
        public BuildingService(IDataService dataService)
        {
             _dataService = dataService;
        }
    
        public IEnumerable<Building> GetBuildings(int propertyId)
        {
            if (propertyId < 0)
            {
                throw new ArgumentException("Negative id not allowed");
            }
    
            if (propertyId == 0)
            {
                return Enumerable.Empty<Building>();
            }
    
            return _myDataService.GetBuildingsOfProperty(int propertyId);
        }
    }
    

    在测试中,您将为 IDataService 并将其传递给 建筑服务

    var fakeDataService = new Mock<IDataContext>();
    var serviceUnderTest = new BuildingService(fakeDataService);
    

    然后,您将对以下各项进行测试:

    "Should throw exception when property Id is negative"  
    "Should return empty collection when property Id equals zero"
    "Should return collection of expected buildings when valid property Id is given"
    

    对于最后一个测试用例,您将模拟 IDataService 仅在正确时返回预期建筑 propertyId 提供给 _dataService.GetBuildingsOfProperty 方法

        2
  •  1
  •   Nkosi    6 年前

    为了使模拟返回数据,需要将is设置为在给定输入的情况下按预期运行。

    当前在测试的方法中,它被如下调用

    buildings = context
      .ExecuteQuery<Building>(System.Data.CommandType.StoredProcedure, "dbo.Building_List", Params.ToArray<object>())
      .AsQueryable();
    

    然而,在测试中,模拟上下文的设置如下

    _mockDataContext.Setup(r => 
        r.ExecuteQuery<Building>(CommandType.StoredProcedure, "someSproc"))
        .Returns(new List<Building>() { new Building() { BuildingId = 1, Title = "1" }}.AsQueryable());
    

    请注意,mock被告知期望参数是什么。

    只有在提供这些参数时,模拟才会按预期运行。否则将返回null。

    考虑以下示例,说明如何根据原始问题中提供的代码执行测试。

    [Test]
    public void SomeTestName() {
        //Arrange
        var expected = new List<Building>() { new Building() { BuildingId = 1, Title = "1" }}.AsQueryable();
        _mockDataContext
            .Setup(_ => _.ExecuteQuery<Building>(CommandType.StoredProcedure, It.IsAny<string>(), It.IsAny<object[]>()))
            .Returns(expected);
    
        DataContext.SetTestableInstance(_mockDataContext.Object);
        var subject = new BuildingService();
    
        //Act
        var actual = subject.GetBuildings(1);
    
        // Assert...
        CollectionAssert.AreEquivalent(expected, actual);
    }
    

    这就是说,当前被测系统的设计与静态依赖关系紧密耦合,这种依赖关系是一种代码气味,使得当前的设计遵循一些不好的做法。

    静态 DataContext 当前用作工厂的应进行重构,

    public interface IDataContextFactory {
        IDataContext CreateInstance();
    }
    

    并显式注入依赖类,而不是调用静态工厂方法

    public class BuildingService : IBuildingService {
    
        private readonly IDataContextFactory factory;
    
        public BuildingService(IDataContextFactory factory) {
            this.factory = factory
        }
    
        public IQueryable<Building> GetBuildings(int propertyId) {
            IQueryable<Building> buildings;
    
            using (var context = factory.CreateInstance()) {
                var Params = new List<SqlParameter> {
                    new SqlParameter("@PropertyId", propertyId)
                };
    
                buildings = context
                  .ExecuteQuery<Building>(System.Data.CommandType.StoredProcedure, "dbo.Building_List", Params.ToArray<object>())
                  .AsQueryable();
            }
    
            return buildings;
        }
    }
    

    这将允许在中创建一个适当的模拟,并将其注入到被测对象中,而无需使用静态解决方法。

    [Test]
    public void SomeTestName() {
        //Arrange
        var expected = new List<Building>() { new Building() { BuildingId = 1, Title = "1" }}.AsQueryable();
        _mockDataContext
            .Setup(_ => _.ExecuteQuery<Building>(CommandType.StoredProcedure, It.IsAny<string>(), It.IsAny<object[]>()))
            .Returns(expected);
    
        var factoryMock = new Mock<IDataContextFactory>();
        factoryMock
            .Setup(_ => _.CreateInstance())
            .Returns(_mockDataContext.Object);
    
        var subject = new BuildingService(factoryMock.Object);
    
        //Act
        var actual = subject.GetBuildings(1);
    
        // Assert...
        CollectionAssert.AreEquivalent(expected, actual);
    }