Skip to main content
Tweeted twitter.com/StackCodeReview/status/1273993983446913024
added tag
Link
forsvarir
  • 11.7k
  • 6
  • 38
  • 71
Source Link
Steve Ngai
  • 925
  • 7
  • 26

Mock Atm program basic functions and xUnit unit test

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?