Slava
Slava

Reputation: 6650

How to mock data repository in Visual Studio Test?

I am new to unit testing. I have these classes AccountBl that calls DataStore that uses SqlConnection to grab data from the database.

I need to test AccountBl.GetInvestmentAccounts method by mocking the data source, the test needs to run even without Database connection.

Here are the given classes 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");
    }
}

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

Here is my 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);


    }
}

When I run, I get error message

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

Obviously its trying to create a connection, but I need to run the test without a connection.

Am I mocking incorrectly?

Upvotes: 3

Views: 2202

Answers (2)

Nkosi
Nkosi

Reputation: 247521

The readonly DataStore dataStore in the subject under test is tightly coupled to the class, making it difficult to test the subject in isolation. You would need to be able to replace that dependency during tests in order to be able to test in isolation.

Consider first abstracting the data store,

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

And having the subject explicitly depend on that abstraction via constructor injection, as classes should depend on abstractions and not on concretions.

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

SqlConnection is an implementation detail that is no longer a concern to the AccountBl

The DataStore implementation would be derived from the abstraction.

public class DataStore : IDataStore {

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

    //...
}

Now that the code has been decoupled, it can be tested in isolation with more flexibility

[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
        //...
    }
}

Upvotes: 1

mittmemo
mittmemo

Reputation: 2090

In your unit test, you create a mock data source but don't use it; that's why DataStore::LoadAcounts is being called. Instead of creating an instance of DataStore in the AccountBl class, you should inject an instance of DataStore in the constructor. This is a form of dependency injection.

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

Now inject the mock data source in the unit test:

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

Upvotes: 0

Related Questions