Bank Exercise
Test project
First, we need to add a Unit Test
project (tests are first).
Tools
We are going to use MSTest to test our code (which is built-in).
You can use other thrid-party tools like xUnit, NUnit etc. (you can install these )
Call it BankTests and name the solution Bank.
Requirements
Then, we need to add a .txt
file called Requirements.
These are the requirements that we need to implement for a bank account:
A BankAccount has:
- number (required)
- balance (initially zero)
Deposit money (amount)
Deny deposit if:
- the amount is negative
Withdraw money (amount)
Deny withdraw if:
- the amount is negative
- the amount is bigger than the balance
These are the things that we need to implement 😀
This is just an example, in real life you have more than these requirements.
TDD
Let's begin by creating a test method for the number
property of the bank account.
Rename the test method to: TestID
.
Create a new bankAccount
variable based on an ID and assert based on the ID:
var bankAccount = new BankAccount(id: 1234);
Assert.AreEqual(1234, bankAccount.ID);
Assert.AreEqual
The first parameter that you pass (in our case 1234
) is the expected value (what you expect to get).
The second parameter is the actual value (the actual value that you get).
If these two match, then you know that your code behaves correctly (you actually get what you expect 😅).
Click the BankAccount
class, press Ctrl + .
and choose Generate Class BankAccount.
internal class BankAccount
{
private int id;
public BankAccount(int id)
{
this.id = id;
}
}
We do the same for the ID
to generate the property.
public int ID { get; internal set; }
IntelliSense
This is the IntelliSense feature of Visual Studio which helps us generate code. 😆
Now, we need to run the test.
You can see that if failed.
Let's make the test pass.
We can see that the ID
property has no connection with the id
field.
We need to assing the value of the id
parameter to the ID
property:
internal class BankAccount
{
public BankAccount(int id)
{
ID = id;
}
public int ID { get; internal set; }
}
Let's run now... and the test passed.
We need to refactor the code:
- make the
BankAccount
public. - put the members in order (first properties, then constructors).
- make the
set
accessor private.
public class BankAccount
{
public int ID { get; private set; }
public BankAccount(int id)
{
ID = id;
}
}
Let's rerun the test to make sure it still passes. And it does.
TIP
We need to apply Unit Tests to make sure our code works as we expect.
XP
TDD is part of eXtreme Programming (XP) which implies that you write tests before the code to be tested.
Now, to write more readable tests, we need a way to separate and show what we are doing: assign some values, act on them to see what they do, and then see if what we wanted happened.
For this, we can use the AAA Pattern:
[TestMethod]
public void TestID()
{
// arrange
var bankAccount = new BankAccount(id: 1234);
// act
var id = bankAccount.ID;
// asssert
Assert.AreEqual(1234, id);
}
In this way, our tests are easy to read, organized and fast for others to understand what is our indent with it. 😄
Shortly:
- in the arrange, we setup all neccesary variables and assign specific values to them
- in the act, we run the code (method, property etc.) that we indent to test
- in the assert, we check if what we expect is what we actually get from the code (if say, then it passes 😄)
Remember
A test method is a method which returns void, has no parameters and has the [TestMethod]
annotation.
Let's create a second test method to test the balance.
We have a shortcut: testm
and name it TestBalance
.
[TestMethod]
public void TestBalance()
{
// arrange
var bankAccount = new BankAccount(id: 1234);
// act
var balance = bankAccount.Balance;
// assert
Assert.AreEqual(0, balance);
}
Notice
As you can see, a Unit Test is a piece of code which tests behaviour of a class member (methods, properties etc.).
Let's generate the Balance
property.
public object Balance { get; internal set; }
Let's run the test... and it failed. The value of Balance
was not 0
, but null
.
Let's make the test pass:
public int Balance { get; internal set; }
We only need to change the type of the Balance
property to int
(the default value of int
is 0).
And let's run the test again... and it passes.
Let's refactor:
- make the type
double
- make the
set
accessor private.
Let's rerun... and it passes.
Refactoring
In plain English, refactoring means that we change our code to increase perfomance (how it runs) and readability (how is written).
The idea with this is to not change too much code so that the tests fail – we just improve these two aspects of the code.
We can also refactor not our code, but the test methods as well.
You can see that 1234
was used twice for the id
and the bankAccount
variable as well.
Imagine doing these for other 30
test methods. 😲
We can use a special method that runs before each test to assign values to these variables.
Here is the method with the neccesary fields:
BankAccount bankAccount;
int id;
[TestInitialize]
public void Initialize()
{
id = 1234;
bankAccount = new BankAccount(id);
}
Then, we can clean up the test methods a little bit:
[TestMethod]
public void TestID()
{
// arrange
// act
var balanceId = bankAccount.ID;
// asssert
Assert.AreEqual(id, balanceId);
}
[TestMethod]
public void TestBalance()
{
// arrange
// act
var balance = bankAccount.Balance;
// assert
Assert.AreEqual(0, balance);
}
Let's rerun to make sure it works. And no problem! 😁
Debugging
You can debug your test methods by setting a breakpoint in the test methods themselves, then click the left-side circle and choose Debug.
We need to continue with the Deposit
method.
So:
testm
andTab
- call it
TestDeposit
[TestMethod]
public void TestDeposit()
{
// arrange
// act
// assert
}
In the arrange, let's declare an amount variable set to 40.50
(this is how much we are about to deposit).
In the act, we call the Deposit
method and pass the amount
.
In the assert, we check to see if the Balance
increased with the amount
.
[TestMethod]
public void TestDeposit()
{
// arrange
var amount = 40.50;
// act
bankAccount.Deposit(amount);
// assert
Assert.AreEqual(amount, bankAccount.Balance);
}
Click on Deposit
to generate the method and run the test. And it fails.
internal void Deposit(double amount)
{
throw new NotImplementedException();
}
So, when we need to increase the Balance
with the amount
:
internal void Deposit(double amount)
{
Balance += amount;
}
Let's run the test... and it passed 👍
Let's refactor:
- make the method
public
Rerun the test.
But we have a condition: the amount must not be negative.
TIP
This is how you should think about testing: start first with the "happy path" and then cover all remaining/edege cases (exceptions, boundaries etc.)
Let's make another test method for that.
So:
testm
andTab
- call it
TestNegativeDeposit
Let's try to deposit -40.99
.
[TestMethod]
public void TestNegativeDeposit()
{
// arrange
var amount = -40.99;
// act
bankAccount.Deposit(amount);
// assert
Assert.AreEqual(0, bankAccount.Balance);
}
We are expecting to get 0
for Balance
because the deposit should be denied.
Let's run and it failed.
So, we need to fix this. We could use an if
statement:
public void Deposit(double amount)
{
if (amount > 0)
{
Balance += amount;
}
}
And this works fine, but we need to be more explicit about what happened.
To do this, we need to throw an exception
– in our case ArgumentOutOfRangeException
because why not 😁
public void Deposit(double amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount must be positive");
}
Balance += amount;
}
It makes our code easier to debug and we can show these message to the user if we need to so that they know why it failed.
But now our test failed. 😥
We need to tell it that we expect an exception to be thrown.
And we can do that:
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestNegativeDeposit()
{
// arrange
var amount = -40.99;
// act
bankAccount.Deposit(amount);
// assert
Assert.AreEqual(0, bankAccount.Balance);
}
Success – it passed (we expect to get this exception because we deposited a negative amount) ✌️
WARNING
Generally don't use try catch
blocks in tests methods – this is why we have ExpectedException
for. :bowtie:
Use try catch
only for testing multiple exceptions in a single test case (and you don't want to have individual test methods which is desired).
Still, you can inherit from Exception
class and make custom expections that you can expect in this way differentiating between exceptions.
Let's continue with the last method: Withdraw
.
So:
testm
andTab
- call it
TestWithdraw
[TestMethod]
public void TestWithdraw()
{
// arrange
var amount = 44.50;
bankAccount.Deposit(100);
// act
bankAccount.Withdraw(amount);
// assert
Assert.AreEqual(55.50, bankAccount.Balance);
}
Let's generate the Withdraw
method.
internal void Withdraw(double amount)
{
throw new NotImplementedException();
}
Run the test and it obviously failed. (nothing crazy yet) 😒
Okay, so let's implement the method:
internal void Withdraw(double amount)
{
Balance -= amount;
}
And it passed. 😌
But, we can see in the requirements we have two conditions:
Deny withdraw if:
- the amount is smaller than the balance
- the balance is negative or 0
So, we need two new test methods:
- TestWithdrawNegativeAmount
- TestWithdrawTooMuch
[TestMethod]
public void TestWithdrawNegativeAmount()
{
}
[TestMethod]
public void TestWithdrawTooMuch()
{
}
We need to check by throwing exceptions as well:
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestWithdrawNegativeAmount()
{
// arrange
var amount = -50;
// act
bankAccount.Withdraw(amount);
// assert
Assert.AreEqual(0, bankAccount.Balance);
}
Run the test and it fails.
Fix the implementation:
internal void Withdraw(double amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount must be positive");
}
Balance -= amount;
}
And it passes. 😏
Let's do the same for the TestWithdrawTooMuch
:
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void TestWithdrawTooMuch()
{
// arrange
var amount = 145.99;
bankAccount.Deposit(100);
// act
bankAccount.Withdraw(amount);
// assert
Assert.AreEqual(100, bankAccount.Balance);
}
This fails. No surprise.
We need to add a new condition:
internal void Withdraw(double amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount must be positive");
}
if (amount > Balance)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount is too much");
}
Balance -= amount;
}
And this passed.
Let's refactor.
I need to:
- make the method
public
And we are done! 😅
All the requirements are implemented.
Behold, this is the BankAccount
class:
public class BankAccount
{
public int ID { get; private set; }
public double Balance { get; private set; }
public BankAccount(int id)
{
ID = id;
}
public void Deposit(double amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount must be positive");
}
Balance += amount;
}
public void Withdraw(double amount)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount must be positive");
}
if (amount > Balance)
{
throw new ArgumentOutOfRangeException(nameof(amount), amount, "The amount is too much");
}
Balance -= amount;
}
}
Now, we need to extract this class and put it into a library.
Let's create a new Class Library
project called Bank.
Press Ctrl + .
on the class and choose the last option to move type....
This extracted the class into a separate file.
Cut and paste the file in the class library (also remove the Class1.cs
).
Go back to the tests and fix the broken reference to the BankAccount
class.
Go to References, then add reference and under Projects, choose the Bank project.
Let's rerun all the tests.
And they all pass.
Now, our class is inside a separate class library that we can share with anyone 😄
Console project
Let's see how our BankAccount
class behaves (we wrote it, now we need to use it).
We can create a simple Console Application to do this (but remembered it can be consumed by any .NET project).
Name it BankConsole.
Let's create a bank account object.
var bankAccount = new BankAccount(9032321);
Add reference to the Bank class library.
We can try to deposit 310.50 and see the Balance
:
bankAccount.Deposit(40);
Console.WriteLine(bankAccount.Balance);
Before we run, we need to set the Console App to be the startup project.
Then run.
And we get 40
.
Let's withdraw 100
.
bankAccount.Withdraw(100);
Console.WriteLine(bankAccount.Balance);
And we got our exception: The amount was too much....
We can use a try catch here to not let the app crash:
try
{
bankAccount.Withdraw(100);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Now, we get a message:
40
The amount is too much
Parameter name: amount
Actual value was 100.
40
Wonderful!
This concludes our journey with TDD 😄
I hope you liked it and will use it in the future! 😉
Check out the source code on GitHub.