c# – 单元测试 – 嘲笑有些困难

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

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返回的对象的值.

最佳答案 看一下下面的例子

[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中提供的示例量身定制的,因此可能需要对其进行一些修改才能应用于您的特定方案.这应该足以让你适应这种情况.

点赞