使用MsTest在C#中模拟SqlConnection、SqlCommand和SqlReader

9

我看到了这个回答,我有兴趣使用 Fake 实现第二个答案。这里还有另一个

我不太理解所有的概念,仍在阅读和理解文档,请问有人可以帮助我使用我的代码,在这里访问客户列表,如何使用 Fake/Shim/Stub/Mock?

如果需要重构以接受依赖项,则您也可以重新编写FindAll方法。

经过讨论后进行了编辑

public class Data
{
    private Func<IDbConnection> Factory { get; }

    public Data(Func<IDbConnection> factory)
    {
        Factory = factory;
    }

    public IList<Customer> FindAll()
    {
        using (var connection = Factory.Invoke())
        {
            const string sql = "SELECT Id, Name FROM Customer";
            using (var command = new SqlCommand(sql, (SqlConnection) connection))
            {
                command.Connection.Open();
                using (var reader = command.ExecuteReader())
                {
                    IList<Customer> rows = new List<Customer>();
                    while (reader.Read())
                    {
                        rows.Add(new Customer
                        {
                            Id = reader.GetInt32(reader.GetOrdinal("Id")),
                            Name = reader.GetString(reader.GetOrdinal("Name"))
                        });
                    }
                    return rows;
                }
            }
        }
    }
}

客户

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

单元测试

[TestMethod]
public void TestDB()
{
    var commandMock = new Mock<IDbCommand>();

    var readerMock = new Mock<IDataReader>();
    commandMock.Setup(m => m.ExecuteReader()).Returns(readerMock.Object).Verifiable();

    var parameterMock = new Mock<IDbDataParameter>();
    commandMock.Setup(m => m.CreateParameter()).Returns(parameterMock.Object);
    commandMock.Setup(m => m.Parameters.Add(It.IsAny<IDbDataParameter>())).Verifiable();

    var connectionMock = new Mock<IDbConnection>();
    connectionMock.Setup(m => m.CreateCommand()).Returns(commandMock.Object);

    var data = new Data(() => connectionMock.Object);
    var result = data.FindAll();
    Console.WriteLine(result);
}

错误

在依赖关系上出了些问题,添加了 System.Data.SqlClient,接着又出现了另一个错误。

System.InvalidCastException: 无法将类型为 'Castle.Proxies.IDbConnectionProxy' 的对象强制转换为类型 'System.Data.SqlClient.SqlConnection'。

指向这行代码

using (var command = new SqlCommand(sql, (SqlConnection) connection))

2个回答

9

被测试的目标方法应该重构以依赖于抽象,而不是实现细节。

例如:

public class Data {
    private Func<IDbConnection> Factory { get; }

    public Data(Func<IDbConnection> factory) {
        Factory = factory;
    }

    public IList<Customer> FindAll() {
        using (IDbConnection connection = Factory.Invoke()) {
            const string sql = "SELECT Id, Name FROM Customer";
            using (IDbCommand command = connection.CreateCommand()) {                    
                command.CommandText = sql;

                connection.Open();
                using (IDataReader reader = command.ExecuteReader()) {
                    IList<Customer> rows = new List<Customer>();
                    while (reader.Read()) {
                        rows.Add(new Customer {
                            Id = reader.GetInt32(reader.GetOrdinal("Id")),
                            Name = reader.GetString(reader.GetOrdinal("Name"))
                        });
                    }
                    return rows;
                }
            }
        }
    }
}

从那里开始,我们可以对这些抽象进行模拟,以便在单元测试隔离时表现如预期。

[TestClass]
public class DataTests{
    [TestMethod]
    public void Should_Return_Customer() {
        //Arrange
        var readerMock = new Mock<IDataReader>();

        readerMock.SetupSequence(_ => _.Read())
            .Returns(true)
            .Returns(false);

        readerMock.Setup(reader => reader.GetOrdinal("Id")).Returns(0);
        readerMock.Setup(reader => reader.GetOrdinal("Name")).Returns(1);

        readerMock.Setup(reader => reader.GetInt32(It.IsAny<int>())).Returns(1);
        readerMock.Setup(reader => reader.GetString(It.IsAny<int>())).Returns("Hello World");

        var commandMock = new Mock<IDbCommand>();            
        commandMock.Setup(m => m.ExecuteReader()).Returns(readerMock.Object).Verifiable();

        var connectionMock = new Mock<IDbConnection>();
        connectionMock.Setup(m => m.CreateCommand()).Returns(commandMock.Object);

        var data = new Data(() => connectionMock.Object);

        //Act
        var result = data.FindAll();

        //Assert - FluentAssertions
        result.Should().HaveCount(1);
        commandMock.Verify(); //since it was marked verifiable.
    }
}

进行集成测试时,可以使用实际的数据库连接来验证功能。

var data = new Data(() => new SqlConnection("live connection string here"));

在生产环境中将使用相同的方法连接到服务器。


这就是为什么我遇到了缺少依赖项的错误,因为我的SUT正在使用System.Data.SqlClient - AppDeveloper
1
@AppDeveloper 是的,紧耦合。这就是为什么我更喜欢抽象化实现方面的问题。大大简化了事情。 - Nkosi
哦,太棒了,非常感谢!这是我在C#中第一次实现模拟,我会继续探索并在遇到问题时发布另一个问题。再次感谢。 - AppDeveloper
@AppDeveloper 参考 https://dev59.com/EZffa4cB1Zd3GeqP95WW#43422083 - Nkosi
1
这里还有一些我使用这种方法的例子 https://stackoverflow.com/search?tab=votes&q=user%3a5233410%20IDbConnection - Nkosi

1
这个问题在于IDbCommand没有ExecuteNonQueryAsync方法,也没有其他任何异步方法。
为什么我要删除异步功能来进行测试?

如果您使用Dapper,您将获得此异步功能。我看到的唯一不是异步的事情是IDbConnection上没有OpenAsync选项。但是,如果您查看SqlConnection正在执行的操作,它实际上并不是异步的。 - zlangner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接