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

时间:2018-04-05 22:17:30

标签: c# unit-testing nunit moq

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

这是我想要进行单元测试的示例方法( GetBuildings ):

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

}

所以 GetBuildings 会调用存储过程。

所以我需要模拟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类型方法(即获取,插入,删除)。

更新

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

2 个答案:

答案 0 :(得分:1)

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

目前在被测试的方法中,它被称为

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

注意模拟被告知期望作为参数。

模拟只有在提供这些参数时才会按预期运行。否则它将返回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);
}

答案 1 :(得分:1)

不要模仿DataContext。因为模拟DataContext将产生与DataContext的实现细节紧密耦合的测试。并且您将被迫更改代码中的每个更改的测试,甚至行为将保持不变。

而是引入一个&#34; DataService&#34;接口并在BuildingService的测试中模拟它。

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

然后,您可以测试IDataService实数数据库的实现,作为集成测试的一部分,或者再次测试内存中的数据库。

如果您可以使用&#34; InMemory&#34;进行测试数据库(EF Core或Sqlite) - 然后更好 - &gt;针对BuildingService的实际实施编写DataContext的测试。

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

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

更新后:

根据更新的问题,BuildingService有一些执行路径 - 您仍然可以测试BuildingService并将与数据相关的逻辑抽象到IDataService

例如,下面是BuildingService class

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创建一个模拟并将其传递给BuildingService的构造函数

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时,您才会模拟extension String { var boolValue: Bool { return (self as NSString).boolValue } } 返回预期的建筑物