Recitation 8: Understanding Mutation
Goals: The goals of this lab are to learn how to design methods that change the state of on object or a state of a data structure, as well as how to design tests that verify the effects of such methods.
To understand mutation we need to learn how to:
Design methods that modify the state of an object
Design tests for effectful methods
The Problem
For this lab we will work with bank accounts. For our purposes we have savings accounts, which must maintain a positive balance, checking accounts, which require a minimum balance (not zero), and credit lines (for borrowing a limited amount of money), which records the balance currently owed and the maximum the customer can borrow.
The bank has a list of Accounts where a customer may deposit or withdraw money. A withdrawal from an account cannot reduce the balance below the minimum, and, for credit lines, cause the balance owed to be above the maximum limit. When a customer deposits money to an account, the balance increases, though for a credit line this decreases the amount owed, which cannot drop below zero.
Methods that effect a simple state change.
Start a new project named Lab8-Bank and import into it the files from the Lab8-Bank.zip file above.
Make examples of Checking, Savings, and Credit accounts.
We’ve started you off with two of the examples using a different organization than you are used to. We use a reset() method to initialize the examples, rather than initializing them when first defined. Follow the same organization with your examples... more on that later.
Discuss several scenarios of making deposits and withdrawals with you partner for each type of account. Make sure you understand when the transaction cannot be completed (i.e., is invalid).
Add the method withdraw to the Account class and implement it in each subclass:
// EFFECT: Withdraw the given amount
// Return the new balance
abstract int withdraw(int amount);
When doing so we encounter a few questions:
Question: How do we signal that the transaction cannot be completed?
Answer: throw a new RuntimeException similar to the following:
throw new RuntimeException("Over credit limit");
Make the message meaningful for your class. You may add some information about the account that caused the problem, the customer name, or the current balance available.
Question: How do we test that the method throws the expected exception?
Answer: This is similar the the constuctor exception tests we have done earlier. Suppose the method invocation:
this.check1.withdraw(1000)
should throw a RuntimeException with the message: "1000 is not available". Our test for this exception would then be:
t.checkException("Testing withdraw checking",
new RuntimeException("1000 is not available"),
this.check1,
"withdraw",
1000);
The first argument is a String that describes what we are testing — it is optional and can be left out. The second argument provides an instance of the Exception our method invocation should throw (the messages must match exactly). The third argument is the instance that invokes the method, the fourth argument is the method name.
After that we list as many arguments as the method consumes, separated by commas.
Question: How do we test the correct method behavior when the transaction goes through?
Answer: We look at the purpose and effect statements. Because the method produces a value as well an effect (changes the state of the object), we must test both aspects.
We first define the instances of the original and expected accounts and add them to our reset method. Inside the test... method we use the reset method to initialize our data since the examples may change during each test.
// Test the withdraw method(s)
void testWithdraw(Tester t){
reset();
t.checkExpect(check1.withdraw(25), 75);
t.checkExpect(check1, new Checking(1, 75,
"First Checking Account", 20));
reset();
}
Notice that we use the reset method twice. At the start we make sure that the data we use has the correct values before the tests are invoked, and after the test(s) we reset the data to the original values. (Since we always reset before testing, we can leave the second reset off. In a more complex situation only our test program may know how to undo the changes our methods have done, and then the test should be followed by a method typically known as tearDown that restores the state of the program to the same state it was in before the test.)
In this case there are two tests we have to perform: the first is what we have done in the past — comparing the value produced by the method with the expected value. The second test verifies that the state of the object did indeed change as expected.
Try the following incorrect implementations of the withdraw method in the Checking class to see why all this testing is necessary:
// Missing Effect...
int withdraw(int amount){
return this.balance - amount;
}
// Wrong return value...
int withdraw(int amount){
this.balance = this.balance - amount;
return amount;
}
// Correct, but no exception!
int withdraw(int amount){
this.balance = this.balance - amount;
return this.balance;
}
Of course, we need to implement and test the method in each Account class: the Savings and Credit classes as well.
Add the method deposit to the Account class and implement it in all subclasses. Remember, what happens in the Credit case when the balance would become negative (no more debt)?
// EFFECT: Deposit the given funds into this account
// Return the new balance
abstract int deposit(int funds);
Make sure your tests are defined carefully as before.
Methods that change the state of structured data.
The Bank class keeps track of all accounts.
Design the method openAcct to Bank that allow the customer to open a new account in the bank.
// EFFECT: Add a new account to this Bank
void add(Account acct){ ... }
Make sure you design your tests carefully.
Note: Here the method does not produce any value and so inside of the test method we first invoke the reset method to initialize all data to the desired values, then invoke the method we wish to test, and then test the effects of the method - and (possibly) invoke reset again.
Design the method deposit that deposits the given amount to the account with the given account number. Make sure you take exception to any problems, e.g., no such account, or a transaction that cannot be completed.
Make sure you design your tests carefully.
Design the method withdraw that withdraws the given amount from the account with the given account number. Make sure you take exception to any problems, e.g., no such account, or a transaction that cannot be completed.
Make sure you design your tests carefully.
Design the method removeAccount that will remove the account with the given account number from the list of accounts in this Bank.
// EFFECT: Remove the given account from this Bank
void removeAccount(int acctNo){ ... }
Hint: Throw an exception if the account is not found, and follow the Design Recipe!