如何在 Visual Studio 测试中模拟数据存储库?

How to mock data repository in Visual Studio Test?

我是单元测试的新手。我有这些 类 AccountBl 调用 DataStore 使用 SqlConnection 从数据库中获取数据。

我需要通过模拟数据源来测试 AccountBl.GetInvestmentAccounts 方法,即使没有数据库连接,测试也需要 运行。

这里是给定的 类 AccountBl:

public class AccountBl
{
    private readonly DataStore dataStore = new DataStore();

    public List<Account> GetInvestmentAccounts(int clientId, AccountType accountType)
    {
        if (accountType == AccountType.Investment)
        {
            var accounts = dataStore.LoadAccounts(clientId);
            return accounts.Where(a => a.AccountType == AccountType.Investment).ToList();
        }
        throw new Exception("Invalid account type provided");
    }
}

DataStore:

public class DataStore
{
    public static string GetAccountsSql = "irrelevant query";

    public virtual List<Account> LoadAccounts(int clientId)
    {
        using (var connection = CreateConnection())
        {
            var sqlCommand = connection.CreateCommand();
            sqlCommand.CommandText = GetAccountsSql;
            sqlCommand.CommandType = CommandType.Text;
            sqlCommand.Parameters.Add("@clientId", clientId);

            var reader = sqlCommand.ExecuteReader();
            var accounts = new List<Account>();
            while (reader.Read())
            {
                var account = new Account();
                account.AccountNumber = (string)reader["number"];
                account.AccountOwner = clientId;
                if (reader["accountType"] == null || reader["accountType"] == DBNull.Value)
                {
                    account.AccountType = AccountType.Checking;
                }
                else
                {
                    account.AccountType =
                        (AccountType)Enum.Parse(typeof(AccountType), reader["accountType"].ToString());
                }
                accounts.Add(account);
            }
            return accounts;
        }

    }

    private static SqlConnection CreateConnection()
    {
        var sqlConnection = new SqlConnection(ConfigurationManager.AppSettings["ConnectionString"]);
        sqlConnection.Open();
        return sqlConnection;
    }
}

这是我的 TestClass

[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void GetInvestmentAccountsTest()
    {
        var clientId = 25;

        var mockAccounts = new List<Account>
        {
            new Account{AccountNumber = "aaa", AccountOwner = clientId, AccountType = AccountType.Investment},
            new Account{AccountNumber = "bbb", AccountOwner = clientId, AccountType = AccountType.Savings},
            new Account{AccountNumber = "ccc", AccountOwner = clientId, AccountType = AccountType.Checking},
        };

        var mockDatastore = new Mock<DataStore>();
        mockDatastore.Setup(x => x.LoadAccounts(clientId)).Returns(mockAccounts);

        var accountBl = new AccountBl();

        var accounts = accountBl.GetInvestmentAccounts(clientId, AccountType.Investment);


    }
}

当我 运行 时,我收到错误消息

Message: Test method ScreeningSample.Tests.UnitTest1.GetInvestmentAccountsTest threw exception: System.InvalidOperationException: The ConnectionString property has not been initialized.

显然它正在尝试创建连接,但我需要 运行 在没有连接的情况下进行测试。

我是不是调错了?

在您的单元测试中,您创建了一个模拟数据源但没有使用它;这就是 DataStore::LoadAcounts 被调用的原因。与其在 AccountBl class 中创建 DataStore 的实例,不如在构造函数中注入 DataStore 的实例。这是依赖注入的一种形式。

public class AccountBl
{
    private DataStore _dataStore;

    public AccountBl(DataStore dataStore)
    {
        _dataStore = dataStore;
    }

    public List<Account> GetInvestmentAccounts(int clientId, AccountType accountType)
    {
        if (accountType == AccountType.Investment)
        {
            var accounts = _dataStore.LoadAccounts(clientId);
            return accounts.Where(a => a.AccountType == AccountType.Investment).ToList();
        }
        throw new Exception("Invalid account type provided");
    }
}

现在在单元测试中注入模拟数据源:

var mockDatastore = new Mock<DataStore>();
mockDatastore.Setup(x => x.LoadAccounts(clientId)).Returns(mockAccounts);

// Inject mock data source
var accountBl = new AccountBl(mockDataStore.Object);

var accounts = accountBl.GetInvestmentAccounts(clientId, AccountType.Investment); 

被测对象中的readonly DataStore dataStore与class紧密耦合,很难单独测试对象。您需要能够在测试期间替换该依赖项,以便能够进行隔离测试。

考虑先抽象数据存储,

public interface IDataStore {
    List<Account> LoadAccounts(int clientId);
}

并且让主题通过构造函数注入明确依赖于该抽象,因为 classes 应该依赖于抽象而不是具体化。

public class AccountBl {
    private readonly IDataStore dataStore;

    public AccountBl(IDataStore dataStore) {
        this.dataStore = dataStore;
    }

    public List<Account> GetInvestmentAccounts(int clientId, AccountType accountType) {
        if (accountType == AccountType.Investment) {
            var accounts = dataStore.LoadAccounts(clientId);
            return accounts.Where(a => a.AccountType == AccountType.Investment).ToList();
        }
        throw new Exception("Invalid account type provided");
    }
}

SqlConnectionAccountBl

不再关心的实现细节

DataStore 实现将从抽象派生。

public class DataStore : IDataStore {

    public List<Account> LoadAccounts(int clientId) {
        //...code removed for brevity
    }

    //...
}

现在代码已经解耦,可以更加灵活的隔离测试

[TestClass]
public class UnitTest1 {
    [TestMethod]
    public void GetInvestmentAccountsTest() {
        //Arrange
        var clientId = 25;
        var mockAccounts = new List<Account> {
            new Account{AccountNumber = "aaa", AccountOwner = clientId, AccountType = AccountType.Investment},
            new Account{AccountNumber = "bbb", AccountOwner = clientId, AccountType = AccountType.Savings},
            new Account{AccountNumber = "ccc", AccountOwner = clientId, AccountType = AccountType.Checking},
        };

        var mockDatastore = new Mock<IDataStore>();
        mockDatastore.Setup(_ => _.LoadAccounts(clientId)).Returns(mockAccounts);

        var subject = new AccountBl(mockDatastore.Object);

        //Act
        var accounts = subject.GetInvestmentAccounts(clientId, AccountType.Investment);

        //Assert
        //...
    }
}