For self learning purpose, I have created some xUnit unit test for these 2 Atm classes. At the moment, data is store in the in-memory object. This version is extended from previous version. For brevity, I excluded Withdraw
method and ThirdPartyTransfer
public class BankAccount : IBankAccount
{
public int Id { get; private set; }
public int BankAccountNo { get; private set; }
public decimal Balance { get; private set; }
public BankAccount()
{
}
public BankAccount(int BankAccountNo, decimal Balance)
{
this.BankAccountNo = BankAccountNo;
if(Balance <= 0)
{
throw new ArgumentException("Create bank account failed. Balance should be more than zero.");
}
this.Balance = Balance;
}
public void Deposit(BankTransaction bankTransaction)
{
if (bankTransaction.TransactionAmount <= 0)
{
throw new ArgumentException("Deposit failed. Transaction amount is more than account balance.");
}
this.Balance += bankTransaction.TransactionAmount;
// Insert transaction record at BankTransaction Repository class
}
public void Withdraw(BankTransaction bankTransaction)
{
if (bankTransaction.TransactionAmount <= 0)
{
throw new ArgumentException("Withdraw failed. Transaction amount is more than account balance.");
}
if (bankTransaction.TransactionAmount > this.Balance)
{
throw new ArgumentException("Withdraw failed. Transaction amount is more than account balance.");
}
this.Balance -= bankTransaction.TransactionAmount;
// Insert transaction record at BankTransaction Repository class
}
}
public class BankTransaction
{
public int Id { get; set; }
public decimal TransactionAmount { get; set; }
public TransactionTypeEnum TransactionType { get; set; }
public int BankAccountId { get; set; }
public BankTransaction(decimal TransactionAmount)
{
this.TransactionAmount = TransactionAmount;
}
}
public enum TransactionTypeEnum
{
Deposit, Withdraw, ThirdPartyTransfer
}
public class BankTransactionRepository : IBankTransactionRepository
{
// Mock DB
public List<BankTransaction> bankTransactions { get; private set; }
public BankTransactionRepository()
{
bankTransactions = new List<BankTransaction>();
}
public void InsertTransaction(BankTransaction bankTransaction)
{
bankTransactions.Add(bankTransaction);
}
public List<BankTransaction> SearchTransactionByDates(DateTime? startDate, DateTime? endDate)
{
if((startDate == null && endDate != null)
|| (startDate != null && endDate == null))
{
throw new ArgumentNullException("Start date or end date should not be null");
}
if (startDate > endDate)
{
throw new ArgumentException("Start date should not be greater than end date");
}
// If both also null, return all.
// todo: add LINQ to filter start and end date before return
return bankTransactions;
}
}
And here are my xUnit unit test methods and tiny bit of Fluent Assertions.
public class BankAccountTest
{
private BankAccount _bankAccount;
public BankAccountTest()
{
_bankAccount = new BankAccount();
}
[Theory, MemberData(nameof(BankAccountConstructorShouldPass_Data))]
public void BankAccountConstructorShouldPass(BankAccount account, BankAccount accountExpected)
{
// Act
_bankAccount = new BankAccount(account.BankAccountNo, account.Balance);
// Assert
//Assert.True(accountExpected.Equals(_bankAccount));
// Doesn't work due to object needs to be serialized first before compare.
// Fluent Assertions
accountExpected.Should().BeEquivalentTo(_bankAccount);
// Default (Without Fluent Assertions)
Assert.Equal(accountExpected.Balance, _bankAccount.Balance);
}
[Fact]
public void BankAccountConstructorInvalidBalanceShouldFail()
{
// Act
var bankAccountNo = new Random().Next();
var balance = -1;
BankAccount TestCode() => new BankAccount(bankAccountNo, balance);
// Assert
var exception = Assert.Throws<ArgumentException>(TestCode);
Assert.StartsWith("Create bank account failed. Balance should be more than zero.", exception.Message);
}
#region "TheoryData"
public static TheoryData<BankAccount, BankAccount> BankAccountConstructorShouldPass_Data()
{
return new TheoryData<BankAccount, BankAccount>
{
{
new BankAccount(123, 250.00M),
new BankAccount(123, 250.00M)
},
{
new BankAccount(321, 150.50M),
new BankAccount(321, 150.50M)
}
};
}
public static TheoryData<BankAccount, BankTransaction, BankAccount> DepositShouldPass_Data()
{
return new TheoryData<BankAccount, BankTransaction, BankAccount>
{
{
new BankAccount(123, 250.00M),
new BankTransaction(50.00M),
new BankAccount(123, 300.00M)
},
{
new BankAccount(321, 150.50M),
new BankTransaction(10.50M),
new BankAccount(321, 160.00M)
}
};
}
#endregion
}
public class BankTransactionsTest
{
private BankTransactionRepository _bankTransaction;
public BankTransactionsTest()
{
_bankTransaction = new BankTransactionRepository();
}
// Arrange
[Theory, MemberData(nameof(InsertTransaction_InsertShouldPass_Data))]
public void InsertTransaction_InsertShouldPass(BankTransaction trans, List<BankTransaction> expected)
{
// Act
_bankTransaction.InsertTransaction(trans);
// Assert
Assert.Equal(expected.Count, _bankTransaction.bankTransactions.Count);
// Fluent Assertions to check if trans is in 'expected' list.
// todo: got issue here.
//expected.Should().Contain(trans);
}
// Arrange
[Theory, MemberData(nameof(SearchTransactionByDates_NullDatesShouldFail_Data))]
public void SearchTransactionByDates_NullDatesShouldFail(DateTime? startDate, DateTime? endDate)
{
Assert.Throws<ArgumentNullException>(() =>
_bankTransaction.SearchTransactionByDates(startDate, endDate));
}
// Arrange
[Theory, MemberData(nameof(SearchTransactionByDates_StartDateGreaterThanEndDateShouldFail_Data))]
public void SearchTransactionByDates_StartDateGreaterThanEndDateShouldFail(DateTime? startDate, DateTime? endDate)
{
Assert.Throws<ArgumentNullException>(() =>
_bankTransaction.SearchTransactionByDates(startDate, endDate));
}
public static TheoryData<BankTransaction, List<BankTransaction>>
InsertTransaction_InsertShouldPass_Data()
{
return new TheoryData<BankTransaction, List<BankTransaction>>
{
{
new BankTransaction(200.00M),
new List<BankTransaction>(){new BankTransaction(200.00M)}
},
{
new BankTransaction(50.50M),
new List<BankTransaction>(){new BankTransaction(50.50M)}
},
};
}
public static TheoryData<DateTime?, DateTime?>
SearchTransactionByDates_NullDatesShouldFail_Data()
{
return new TheoryData<DateTime?, DateTime?>
{
{ null, new DateTime(2020,06,09) },
{ new DateTime(2020,06,09), null },
};
}
public static TheoryData<DateTime?, DateTime?>
SearchTransactionByDates_StartDateGreaterThanEndDateShouldFail_Data()
{
return new TheoryData<DateTime?, DateTime?>
{
{ new DateTime(2020,06,09), new DateTime(2020,06,08) }
};
}
}
Any comments of the code structure, coding style and best practices?