Now let's take a look at testing some other classes in our application.
If you go into the BankService, let's drill into the “SavingsAccount” class. Now first we'll examine this to kind of look at some different constructs that are being used in this particular class.
You will see that SavingsAccount extends Account
. This extends
keyword is how we denote inheritance in Java. And what it's saying is that “SavingsAccount” is a special type of “Account”, which is a more general class.
And so, let's take a look at Account first to understand that basic functionality.
/**
* Abstracts the general concepts of a bank account.
* @author Tariq King
*/
public abstract class Account {
protected long accountNumber;
protected double balance;
protected Customer owner;
public Account(Customer owner, double balance, long accountNumber) {
this.owner = owner;
this.balance = balance;
this.accountNumber = accountNumber;
}
public Customer getOwner() { return this.owner; }
public long getAccountNumber() { return this.accountNumber; }
public double getBalance() {
return this.balance;
}
public void deposit(double amount) throws InvalidAmountException {
if (amount <= 0) {
throw new InvalidAmountException();
}
else {
this.balance = this.balance+amount;
}
}
public abstract void withdraw(double amount) throws InsufficientFundsException;
}
And here we see something unique as well, Account is defined as an abstract class
.
An abstract class
is one in which some aspect of the implementation is deferred until later. So here, in method, “withdraw”, is actually not implemented. There's no instructions or body. There's just an interface that declares what the signature of this method looks like. Therefore, classes that inherit from Account, will need to implement this method
before they can be instantiated as objects.
Okay, so what's an Account?
Well we have the constructor.
And so, we have a constructor taking a customer. And so, we see that account is really one of the clients of customer. And so, it uses customer to define an owner. You also instantiate it or initialize it with a balance and then an account number.
And so, these are the three attributes.
These attributes are listed as protected
. So far, we've been familiar with public
and private
.
Protected attributes are those which are not quite private, because they're still accessible by other classes in the inheritance hierarchy. In other words, classes that extend Account, will be able to access the account number, the balance and the owner. But any classes that are outside of that inheritance chain will not have access to these protected members.
So, there's also your standard public
interface where you have the ability to get
your owner of the account, retrieve your account number, your balance and then there's a deposit method.
In here we also see the deposit
method throws
an exception — InvalidAmountException
.
In other words, there's some checking here because this class was designed defensively. And so, what we're saying is that we want to be able to protect against invalid inputs.
And so, here we see a check that says, "If the amount is less than zero". In other words, if we were trying to deposit a negative amount, then we'll throw an invalid amount exception.
Otherwise we'll go ahead with the deposit. And then there's that abstract withdrawal method.
So now that we understand account, let's look at savings account.
public class SavingsAccount extends Account {
private double annualInterestRate;
private final double defaultAnnualInterestRate = 0.0005;
private double unpaidInterest;
public SavingsAccount(Customer owner, double startBalance, long accountNumber) {
super(owner, startBalance, accountNumber);
this.annualInterestRate = defaultAnnualInterestRate;
this.unpaidInterest = 0.0;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > this.balance) {
throw new InsufficientFundsException();
}
else {
this.balance = this.balance - amount;
}
}
public void calculateUnpaidInterest(){
unpaidInterest = unpaidInterest + this.balance * annualInterestRate;
}
public double getUnpaidInterest() { return unpaidInterest; }
public void payInterest(){
try {
deposit(unpaidInterest);
} catch (InvalidAmountException e) {
e.printStackTrace();
}
}
public double getAnnualInterestRate() {
return annualInterestRate;
}
public void setAnnualInterestRate(double annualInterestRate){
this.annualInterestRate = annualInterestRate;
}
}
And in particular we start to see some new attributes, interest rates and so on.
But we also see that the withdrawal method has now been implemented. And there, it also throws
an exception, an “InsufficientFundsException”.
And so here it looks like, in the savings account we're checking to see if the amount is greater than the balance then we will not allow that withdrawal to go through, and we will just throw insufficient funds.
Otherwise we'll go ahead with the withdrawal.
And so, let's see how this differs from a checking account.
/**
* Specialized bank account on which checks can be processed.
* @author Tariq King
*/
public class CheckingAccount extends Account {
private double minBalance;
private double overdraftLimit;
private double overdraftFee;
private double serviceFee;
private boolean isOverDrawn;
private boolean droppedBelowMinBalance;
private boolean notificationsEnabled;
private Notification notificationService;
public CheckingAccount(Customer owner, double startBalance, long accountNumber) {
super(owner, startBalance, accountNumber);
this.minBalance=1500.0;
this.overdraftLimit=0.0;
this.overdraftFee=30.0;
this.serviceFee=12.0;
this.isOverDrawn=false;
this.droppedBelowMinBalance =false;
this.notificationsEnabled=false;
this.notificationService=null;
}
Checking account also inherits from the general Account; and will also therefore have to implement a withdrawal method.
And here we see something slightly different. The behavior is slightly different with a checking account because we're taking into account an overdraft limit that is not present in the savings accounts.
And so, this is one of the key ways that we use inheritance and use abstract classes, is to change behavior slightly when we still want to reuse some common elements. And obviously we have to make sure that we test all of this.
But let's just first start in the context of the savings account and we'll create a class to test the savings account.
Let's create our test — for now, let's focus on testing the withdrawal method.
So, select withdrawal from the list and click OK.
And you'll see now that we've created our template class. It's already gone ahead and populated a method for withdrawal. Now there's not a great name, it just replaced it with a little bit of a stub name, but we'll update all that later.
Let's think about testing the savings account and in particular the withdrawal scenario — and remembering that this particular class is designed using defensive programming.
Therefore, we want to make sure that we consider both positive and negative scenarios. But let's go ahead and define our tests.
/**
* Customers should be able to withdraw from their savings account.
* Scenario:
* 1. Given a customer's savings account with an initial balance of $100.00
* 2. When I withdraw $60.00 from the account
* 3. Then the new account balance is $40.00
*/
And so, here we'll go ahead and do a test saying that, starting with the positive, that customers should be able to withdraw their savings account. And we'll go ahead and start our test steps, so our scenario. And we'll say, given a customer's savings account with an initial balance of $100, when I withdraw $60 from the account then the new account balance is $40.
And so here we have our sunny day scenario for withdrawal.
Let's go ahead and start to define the body here.
@Test
public void withdrawingValidAmountFromSavingsAccount_DecreasesBalanceByAmount() {
}
So, we want to first update this name. And we can say, withdrawing a valid amount from savings account decreases balance by the amount. So, we expect that this would be a positive transaction. And so, we have defined at least the skeleton for this particular test.
Let's go on and see what other scenarios we might want to consider.
So here, we want to start to think about a negative scenario.
In other words, customers should not be able to withdraw more than their available savings account balance.
And so, let's define that.
/**
* Customers should not be able to withdraw more than their available savings account balance
* Scenario:
* 1. Given a customer's savings account with an initial balance of $100.00
* 2. When I attempt to withdraw $200.00
* 3. Then an exception should occur indicating that there are insufficient funds in the account
*/
And here, we can have our steps. The first step being, given a customer's savings account with an initial balance of $100 again, when I attempt to withdraw, let's say, $200, twice that amount, then an exception would occur indicating that there are insufficient funds in the account.
Now we have a scenario that gives us our first look at doing some negative testing using TestNG.
And in particular, here we're going to learn how to handle exceptions.
@Test
public void withdrawingAmountGreaterThanBalance_Throws_InsufficientFundsException() {
// Given
customer = new Customer ("Mickey Mouse", "Disneyland", "Mickey@disneyland.com");
savings = new SavingsAccount(customer, 100.00, 123456789);
}
If we start here, we can define our test, and into our public void
, and we can name our test, withdrawingAmountGreaterThanBalance_Throws_InsufficientFundsException
.
Let's continue. We'll actually get into the implementation of this.
Here in our “Given” section, we can start setting up the customer's savings account. And so, we can say, customer equals new customer. We can setup Micky Mouse as the person who's doing this. And now we need our savings account. And for that we need a new savings account. We have a customer. We'll start with an initial balance of 100 as the investment. Let's just put in an account number.
We can now do the “When” section, where we do “savings.withdraw” and the amount of $200.
And immediately you'll see the compiler complain. It says, “we don't have you handling this exception that can be thrown.”
We call, but if we go back to the savings account, the withdraw method and throw
this “InsufficientFundsException” And so therefore our test must also throw this exception.
@Test
public void withdrawingAmountGreaterThanBalance_Throws_InsufficientFundsException() throws InsufficientFundsException {
// Given
customer = new Customer ("Mickey Mouse", "Disneyland", "Mickey@disneyland.com");
savings = new SavingsAccount(customer, 100.00, 123456789);
}
// When
savings.withdraw(200.00);
So, we can have, throws InsufficientFundsException
. And that'll resolve that.
But we're still not done, because we're still missing that “then” portion — where we are expecting this exception to be thrown.
Now there's a number of different ways you can handle this. And so, I'll show you one where there's a special case where you can use an attribute next to the annotation to denote that you expect an exception to occur.
So, in this case, you wouldn't have a “then” section here, you would just modify this annotation to say
@Test (expectedExceptions = InsufficientFundsException.class)
public void withdrawingAmountGreaterThanBalance_Throws_InsufficientFundsException() throws InsufficientFundsException {
And so, what this does, it tells the compiler that this method, once we execute, should throw this exception. And if it does, it'll trigger TestNG to say, "Well, this test has passed."
And so, let's run that really quickly just to make sure.
And we see that there, works fine.
Now if we were to remove this — (expectedExceptions = InsufficientFundsException.class) — we would expect that this test blew up, because that exception just happens. So, we see that once we have our attribute, our test knows, we will expect that exception to occur and therefore we'll mark that test as passed.
But we still have a bit of a problem. I wonder if you've seen the problem so far.
Well here, we're checking that the exception is thrown, but there's something important that we need to make sure is not happening to the account.
We want to make sure that there's no side-effects happening, where even though the exception is thrown, we need to check to make sure that the balance has not been modified.
And here I'll introduce a new pattern that helps to deal with this sort of thing.
The problem with using this particular attribute is that once this exception gets thrown, nothing else that you put here will get executed. So, we can't really check the balance at this point because once this call happens and the exception is thrown it exits and does this type of validation to see if they expected exception was thrown.
And so here, I'll show you what is a valid use of the _try/catch pattern _when it comes to handling exceptions.
/**
* Customers should not be able to withdraw more than their available savings account balance
* Scenario:
* 1. Given a customer's savings account with an initial balance of $100.00
* 2. When I attempt to withdraw $200.00
* 3. Then an exception should occur indicating that there are insufficient funds in the account
* 4. And the account balance should remain unchanged.
*/
@Test (expectedExceptions = InsufficientFundsException.class)
public void withdrawingAmountGreaterThanBalance_Throws_InsufficientFundsException() throws InsufficientFundsException {
// Given
customer = new Customer ("Mickey Mouse", "Disneyland", "Mickey@disneyland.com");
savings = new SavingsAccount(customer, 100.00, 123456789);
// When
try {
savings.withdraw(200.00);
fail(“Expected Insufficient Funds Exception but none was thrown”);
} catch (InsufficientFundsException e) {
// Then
assertEquals(savings.getBalance(), 100.00);
}
}
And so, let's just modify our description to now include, and the account balance should remain unchanged, because we want to be able to verify that as well.
The way that we do this is by enclosing our call by “try.catch” block.
First we're going to try
to withdraw from the account. And if we try
to withdraw from the account and we're actually successful, we know that something went wrong, because we should get an exception.
And so here, what we'll do is fail
our test and say, expected InsufficientFundsException, but none was thrown. So that handles the try
portion. If we try
it and we actually succeed, then we know we need our test to fail.
However, if we catch
the insufficient funds exception, then ... This is our then. We want to be able to assertEquals
that savings.getBalance
is $100.
And so now we're complete, because if the exception wasn't thrown, our test would fail, otherwise it would pass. And not only will it pass if it drops into this block, but now we also have the additional assertion that that balance remains the same.