Lab 3
Lab 3 Description
The goal of this lab is to think about the design of tests in the presence of mutation. The third part studies the meaning of equality and the design of equality comparison.
Game change
The library impworld.jar aupports the design of the interactive games using imperative style (mutation). The methods onTick, onKeyEvent, OnMouseClisk(Posn p), (and all other mouse handling methods) have the return type void. Each method causes the state of the game to be modified to reflect the desired changes.
The tests for the methods with effects are more difficult. We need to make sure the data we use is set up as desired, invoke the method whose effects we want to test, then run the tests (and possibly, clean up after the test).
The starter files:
The file that contains the images of the shark and the fish used in our game Images.zip.
The complete game written in the functional style ExamplesOceanWorldFun.java.
Start a new project, import the solution in the functional style, as well as all the needed images. Add to the classpath the libraries: worldcanvas.jar, worldimages.jar, impworld.jar, colors.jar, and tester.jar.
Change the import in the solution from import funworld.*; to import impworld.*;
Comment out the tests for all methods except for the class Fish and the class CartPt. Also comment out all class definitions except the interface WorldConstants, class Fish, and class CartPt. Now change the methods in these classes to the imperative style, and modify the tests.
Here is an example:
// In the class Fish:
// EFFECT:
// move the fish to the right side of the ocean
// and reset its size
void start(int y, int size){
this.p.x = WIDTH;
this.p.y = y;
this.size = size;
}
// in the class ExamplesOceanWorldImp:
// test all methods in the class Fish
public void testFishMethods(Tester t){
this.f.start(30, 50);
t.checkExpect(this.f, new Fish(new CartPt(WIDTH, 30), 50, this.f.name));
}
Make sure all test cases pass.
Do the same for the class Shark.
Continue with the other classes. Notice, that to make some tests pass, yoou need to reset the data. To make this easier, our appreoach is to design one or more #tt{reset} methods that just reproduce the original initialization of all data, and invoke this method before each test that may need it.
The solution ExamplesOceanWorldImp.java shows you this technique. Please, note the comments in the method moveLeftRandom in the class CartPt:
//EFFECT:
// Move this CartPt left i units -- or back to the right, if out of bounds
// also move randomly up or down -2 to 2 pixel
void moveLeftRandom(int i) {
if (this.x - i < 0)
this.x = WIDTH;
else{
// our tests did not catch that rand.nextInt(2) did not provide all desired values
this.x = this.x-i;
this.y = this.y + rand.nextInt(5) - 2;
}
// NOTE: My tests found the error in this simple method: I forgot to enclose the
// two statements in the else clause in brackets, and so, the y coordinate
// was changing regardless of whether the fish moved back to the start!!!
}
Understanding mutation
Our goal is to provide examples where mutation is necessary. Our simple scenario involves two kinds of bank accounts, a checking account and a savings account, and the account owners. We need methods that will record deposits, withdrwals, and provide information about available funds.
The starter files:
The file that contains the data definitions for these classes ExamplesBankingData.java.
The bad solution of this problem – written in the functional style ExamplesBankingMethodsBad.java.
The good solution of this problem – written in the imperative style, but with no tests ExamplesBankingMethods.java.
The complete solution of this problem – written in the imperative style. ExamplesBankingMethodsSolution.java.
The Motivation
We start by defining the classes that represent the banking data. The file ExamplesBankingData provides this, including example of data, and the templates for the methods.
Circularity
Think of what would happen to the data definitions and the constructors if every person could have just one account, but the account would include the fields that represented the account owner, and the owner’s data would refer to the owner’s account.
The class diagram would be:
+------------+ +-----------+ |
| | | | |
v | | v |
+--------------+ | | +----------------+ |
| Person | | | | Acct | |
+--------------+ | | +----------------+ |
| String name | | | | int acctNo | |
| Acct account |----+ | int balance | |
+--------------+ | | int minBalance | |
+----| Person owner | |
+----------------+ |
We could not make examples of data without first creating an incomplete object and only later initializing the remaining fields. In this case, the Person would have no account, then when the account was created, the information about if would be provided to the Person object.
This kind of circularity appears in many situations, and is not always as obvious as in this example. The text How to Design Classes has a discussion of this problem in depth. We mentioned it, because it is one of the examples that motivates the need for mutation.
The object has an owner, and so it cannot be replaced
In our example the person knows about the account it owns. If the account instance is replaced by a new one every time we wish to make a change, the account owner will not notice the change.
An object may have more than one owner, which makes test more complex
Our examples allow two or more people to own jointly an account. (In our example mom and dad share the savings account though both have their own checking account.) Here the tests for the effect of the method should verify that the change in the account values has been seen by all owners of the account.
Create a project with the ExamplesBankingMethodsBad.java file in it, importing the tester.jar library. Look at the code, and notice why these examples of methods do not work.
Replace the file with the ExamplesBankingMethods.java and read it through. The tests are simple, designed is such a way that they do not affect ech other. Add more tests - checking the result of withdrawal after a deposit has been made in the previous test. Now the two tests are not independent. The result of the second test depends on a successful execution of the first one.
Think of what needs to be done to make the tests independent.
Replace the file with the ExamplesBankingMethodsSolution.java. It illustrates our very simple and straightforward approach to making test cases independent. The method reset() resets all the instances of data defined in the ExamplesBankingMethodsSolution class to the values initially assigned to them. The test cases then invoke the reset method every time fresh data values are needed. The two failed test cases illustrate the impact of our failure to do so.
Read and add more test cases - or fix the two that we have failed to do correctly.