单元测试 - 嘲笑有些困难

时间:2017-01-23 17:02:03

标签: c# unit-testing moq

我正在使用一些遗留代码,我需要为其编写一些单元测试。存在具有以下签名的数据访问方法。

Task ExecuteReaderAsync(string procedureName, Parameters procedureParameters, 
    params Action<System.Data.IDataReader>[] actions);

在我正在测试的类中有一个类似于

的实现
private async Task<CustomObject> GetCustomObject(int id) 
{
    CustomObject obj = null;
    await db.ExecuteReaderAsync("nameOfProcedure", some parameters, 
    dr =>
    {
        obj = new CustomObject()
        {
            Prop1 = dr["Col1"],
            Prop2 = dr["Col2"]
        }
    }
    return obj;
}

我正在努力控制GetCustomObject返回的值。如果ExecuteReaderAsync实际上已经返回了一些东西,我可以像这样设置。

mockDataAccess.Setup(x => x.ExecuteReaderAsync("nameOfProcedure", It.IsAny<Parameters>()))
    .Returns(Task.FromResult(new CustomeObject() { prop1 = "abc", prop2 = "def"};));

但指定值的逻辑是Action<IDataReader>,我无法控制。我想知道是否有任何技巧可以用来做我想做的事, 即控制GetCustomObject返回的对象的值。

1 个答案:

答案 0 :(得分:2)

看一下以下示例

[TestClass]
public class LegacyCodeTest {
    [TestMethod]
    public async Task TestExecuteReaderAsync() {
        //Arrange
        var mapping = new Dictionary<string, string> {
            { "Col1", "abc" },
            { "Col2", "def" }
        };

        var mockDataReader = new Mock<IDataReader>();
        mockDataReader
            .Setup(m => m[It.IsAny<string>()])
            .Returns<string>(col => mapping[col])
            .Verifiable();

        var mockDataAccess = new Mock<IDataAccess>();
        mockDataAccess
            .Setup(m => m.ExecuteReaderAsync("nameOfProcedure", It.IsAny<Parameters>(), It.IsAny<Action<System.Data.IDataReader>[]>()))
            .Returns(Task.FromResult<object>(null))
            .Callback((string s, Parameters p, Action<System.Data.IDataReader>[] a) => {

                if (a != null && a.Length > 0) {
                    a.ToList().ForEach(callback => callback(mockDataReader.Object));
                }

            })
            .Verifiable();


        var sut = new SUT(mockDataAccess.Object);

        //Act
        var actual = await sut.MUT(2);

        //Assert
        mockDataAccess.Verify();
        mockDataReader.Verify(m => m["Col1"]);
        mockDataReader.Verify(m => m["Col2"]);

        actual.Should()
            .NotBeNull()
            .And
            .Match<CustomObject>(c => c.Prop1 == mapping["Col1"] && c.Prop2 == mapping["Col2"]);

    }

    public interface IDataAccess {
        Task ExecuteReaderAsync(string procedureName, Parameters procedureParameters, params Action<System.Data.IDataReader>[] actions);
    }

    public class Parameters { }

    public class CustomObject {
        public object Prop1 { get; set; }
        public object Prop2 { get; set; }
    }

    public class SUT {
        IDataAccess db;

        public SUT(IDataAccess dataAccess) {
            this.db = dataAccess;
        }

        public async Task<CustomObject> MUT(int id) {
            var result = await GetCustomObject(id);
            return result;
        }

        private async Task<CustomObject> GetCustomObject(int id) {
            CustomObject obj = null;
            await db.ExecuteReaderAsync("nameOfProcedure", null,
            dr => {
                obj = new CustomObject() {
                    Prop1 = dr["Col1"],
                    Prop2 = dr["Col2"]
                };
            });
            return obj;
        }
    }
}

由于您无法控制Action<IDataReader>,在这种情况下,可以做的最多是确保操作不会失败。所以这意味着传递一个模拟阅读器,它可以按照行动的预期执行。

var mapping = new Dictionary<string, string> {
    { "Col1", "abc" },
    { "Col2", "def" }
};

var mockDataReader = new Mock<IDataReader>();
mockDataReader
    .Setup(m => m[It.IsAny<string>()])
    .Returns<string>(col => mapping[col])
    .Verifiable();

通过使用回调来访问传入的参数

.Callback((string s, Parameters p, Action<System.Data.IDataReader>[] a) => {

    if (a != null && a.Length > 0) {
        a.ToList().ForEach(callback => callback(mockDataReader.Object));
    }

})

模拟读者可以传递给被测方法中调用的动作。

此答案适用于OP中提供的示例,因此可能需要对其进行一些修改才能应用于您的特定方案。这应该足以让你适应这种情况。