Reputation: 6650
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
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
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