Navigation
- Introduction to JUnit
- Arithmetic
- Compound Interest
- Multidimensional Arrays
- Starting Signpost
- Submission
A. Introduction to JUnit
In hw1, you saw an example of unit testing, the testing of individual components (methods) of a program. To do this, you write extra code that is not used in the actual operation of your program, but is instead intended for use during development to find and localize bugs as they happen.
Why is testing important?
Let's say we have a giant project to keep track of users' accounts and the overall worth of their stocks. The project comprises 10 classes, each with a wide range of methods and constructors. In testing our project, we find out that one of the user's overall worth for 2020 is off by 19 cents.
Where do we start looking for the problem? Do we look for errors in how we store each user's balance? Do we look for errors in how we calculate interest over time? Determining where the bug is coming from may take longer than writing the program itself!
Testing each individual piece helps us avoid this problem. When we finish our interest calculation method, we write a small test which helps us feel confident that our method is working correctly, and fix whatever bugs we are able to find. When we know that our foundation is solid (that all of the individual methods we wrote should be working correctly), we can move forward to actually using our code without worrying.
Setup
To support unit testing our programs, we'll rely on a widely used testing package called JUnit. Your instructor has been quoted saying that "it is one of the most poorly documented bunches of Java code I've seen," so we'll jump right into using it, rather than going to any official documentation.
As with any assignment, start this homework by running the following commands in your
local repo
directory.
git fetch shared
git merge shared/hw1 -m "start HW 1"
git push
You'll receive a hw1 folder with three subdirectories: Arithmetic
,
CompoundInterest
, and MultiArr
. Arithmetic contains a fully implemented
sample program and JUnit tests, and the other two folders are programs that
you'll need to implement for this homework.
Open the hw1 folder in IntelliJ using Import Project or File > New >
Project from Existing Sources..., and not with the Open command. Remember
to import the libraries in cs61b-software/lib
by File > Project
Structure > Libraries and clicking on the green plus button.
Ad-hoc Testing in Java
Let's start by examining the already completed contents of the
Arithmetic
folder. In it, you'll see a very simple arithmetic package in
a file named Arithmetic.java
, along with a couple of test clients named
ArithmeticTest.java
and ArithmeticJunitTest.java
. In case you're
unfamiliar with the term, a program X is said to be a client of the
program Y if X uses any data or methods from program Y. In this case, the
purpose of our two clients will be to test the class Arithmetic
.
The ArithmeticTest
test client is an ad-hoc test written entirely from
scratch. Don't try to understand the details or even the flow of the
test, just briefly look at the overall structure, noting the length and
the nature of the methods implemented.
You'll observe that the source code is 56 lines long, and has to manually
implement common tasks like approximate floating point comparison,
tallying of tests passed, and provision of useful test output for the
human user. There are various ways to run the tests.
Try running the file ArithmeticTest.java
in IntelliJ. You should
see the output:
product OK.
sum FAILS.
On Unix or MacOS, another alternative is to use the command line to compile
the program with make
(or compile ArithmeticTest.java
with javac
on Windows)
and then run it with java ArithmeticTest
(or do both with make adhoc-check
).
This should give the same output.
JUnit Testing
The JUnit package does a lot of the kludgy work for us, avoiding implementation of common testing tasks such as those we saw in ArithmeticTest. Basic JUnit tests tend to leverage a few key components:
- A set of methods with names like
assertTrue
andassertEquals
that perform some simple tests and cause an error if it fails. - A number of "annotations," such as
@Test
, which marks a method as being a unit test. - Various main testing routines that examine
specific classes at execution time and call all of their annotated test methods,
i.e. those methods with
@Test
proceeding their definition.
As an example, look at the Arithmetic/ArithmeticJUnitTest.java
JUnit-
based arithmetic test client:
- It starts with two lines that begin with
import
. These lines just mean that our program will be able to utilize shorthand names for items in the JUnit libraries: for example,assertEquals
rather thanorg.junit.Assert.assertEquals
. - Some of the methods have
@Test
right above their declaration. This is an example of an annotation which attaches various "metadata" to a Java entity that is then accessible by the Java program itself. As an example, the JUnit framework is a Java program that looks for methods that have the @Test annotation, and then executes each such method found. - The main method performs the task
System.exit(ucb.junit.textui.runClasses(ArithmeticJUnitTest.class))
. This just means that every method in the classArithmeticJUnitTest
that has the annotation @Test is to be run, and the results accumulated and reported.
In this homework, you'll write your own such JUnit tests, which can be compiled and executed with the command make check
.
There are a number of advantages to using JUnit-based testing over the ad-hoc test above: the JUnit test is only 29 lines long, is easier to read, and avoids implementation of common tasks like approximate floating point comparisons, and so forth. Furthermore, when run, it also provides us with a more useful output for deubgging purposes:
Time: 0.018
There were 1 failures:
1) testSum(ArithmeticJUnitTest)
expected:<11.0> but was:<30.0>
at ArithmeticJUnitTest.testSum:14 (ArithmeticJUnitTest.java)
Ran 2 tests. 1 failed.
JUnit tests are easy to write once you learn the basics and give you useful output, straight out of the box. We hope you'll grow to love them.
B. Arithmetic
Open up Arithmetic/ArithmeticJUnitTest.java
. Try looking through the file. Try running the tests. Do they pass or fail? Make sure the test fail and then open Arithmetic/Arithmetic.java
. Look through the code until you find the mistake. Try running your tests again. They should pass now.
C. Compound Interest
"Compound interest is the most powerful force in the universe." - Albert Einstein (maybe)
Investment income grows faster than inflation, and thus the choices you make about investment at an early age can make a huge difference in how much money you'll have when you retire. In this homework problem, we'll build some code to explore this idea, and we'll also get some practice with the idea of test-driven-development using JUnit.
Go into the CompoundInterest folder, and you should see
CompoundInterest.java, CompoundInterestTest.java, and Makefile. Each of
these files is a
skeleton.
Your goal in this problem is to fill in all the methods in both
.java
files to match the comments.
As you work, try to use the test-driven development methodology where you do things in the following order: Write the test. Run the test (you should fail). Write the code. Run the test (you should pass). Refactor if desired and if so, re-run test.
To run the tests in CompoundInterestTest.java
, select the file and click on
Run > Run 'CompoundInterestTest' in IntelliJ. You'll see
that the unit tests report that all tests have passed. This is bad,
because it means that our starter test is garbage, as it believes our
incomplete CompoundInterest.java is flawless.
By the way,
you can also run the starter test from the command line like this (if you have
make
installed):
$ make check
which (as you can see from Makefile
) runs the command
java CompoundInterestTest
after first making sure that CompoundInterestTest.class
is up to date.
The rest of this part of the homework describes a suggested path to completion. You do not have to follow it, but it is recommended. If you set off on your own from this point on to Part 1, please give the test-first approach a fair shake. We strongly believe it will save you grief in the future.
testNumYears
Start this homework by opening CompoundInterestTest.java
and
CompoundInterest.java
in intelliJ. In
CompoundInterestTest.java
, you'll see a bunch of tests you're supposed to
implement. Using Arithmetic/ArithmeticJUnitTest.java
as a guide, edit the testNumYears
method so that it acts as a good test of whether or not numYears
obeys
the specifications given in the documentation comments in
CompoundInterest.java
. Two assertEquals
statements are probably good
enough. We're throwing you right in at the deep end with a bunch of sharks
and megalodons here, so don't hesitate to ask for help (in lab, office
hours, piazza, HKN, etc.).
Useful fact: numYears
returns an int, so you don't need to specify a
tolerance when you write your assertEquals statements (since we don't have
to worry about rounding error when comparing integers).
After you've created better tests, run them, and your
numYears
method should now fail the test. Ironically, this shows that
the test is working! In fact, experienced programmers get suspicious when
they write a bunch of tests that don't fail out of the box.
numYears
Now edit numYears
in CompoundInterest.java so that it passes the test.
It should be a straightforward method to write.
While it might be a little silly to write a unit test for something as trivial as numYears, once you get used to JUnit testing, the time taken to write a test becomes so small that you may as well write at least a basic test for every method. This will save you sweat and tears down the line.
futureValue
Repeat the exercise from before, but now with the testFutureValue
and futureValue
methods. Write the test first, and verify that it compiles and fails before moving on to writing futureValue
. Feel free to use the example in the documentation comments as one of your JUnit tests.
Make sure your test includes negative appreciation rates.
futureValueReal
Now we'll write a method that computes the future value of an appreciating (or depreciating) asset taking inflation into account. Having a million dollars today is very different from what it will be in 60 years.
To correct for inflation, one simply considers how much an asset would be worth if it hypothetically depreciated at the inflation rate for the appropriate time frame. For example, if we want to know how much 1,000,000 dollars in cash will be worth in 40 years and we assume the inflation rate will be 3 percent over the next 40 years, we'd see it would be worth $\$1,000,000 \times (0.97)^{40}$ or $295,712.29 in 2020 dollars. Not bad, but not quite so impressive.
Again, start by writing the tests, then run the tests to see they
successfully compile and fail, and then finally write code for
futureValueReal
that passes the tests.
printDollarFV and CompoundInterest.main
Using what we've written so far, we can answer our first interesting question: How much money is future-you losing every time present-you spends a dollar? They say a penny saved is a penny earned, but this is only true if you're a bad investor. In fact, each penny is worth many pennies.
Try running CompoundInterest's main function, and you'll see that it tells you something that is clearly not true (assuming that we don't go through an apocalyptic event that eradicates the value of all money). Update the printDollarFV function so that it gives you a correct result.
totalSavings
and totalSavingsReal
Another more interesting question: How much money will you have if you set
aside some fixed amount each year? To lay the groundwork, repeat the same
exercise as above for totalSavings
and totalSavingsReal.
printSavingsFV
and CompoundInterest.main
As the final step in this assignment, edit printSavingsFV
so that it
gives you useful information about how much money you'll have if you save
perYear
dollars every year until targetYear
.
D. Multidimensional Arrays
Test-driven development particularly shines when you have a task whose outcome is conceptually easy to understand but hard to implement. Let's try out the TDD methodology in the context of recursive data structures.
This assignment will be diving into the world of multidimenstional arrays! We know that an array is a list of objects (integers, strings, Objects, etc). But what happens when we put an array inside an array? What about when those internal arrays are themselves filled with arrays? It's turtles all the way down!
Indexing into these types of data structures can get confusing as the number of dimensions increases, but if you can draw a mental map of the dimensions the indexing follows. Row indices always comes first, followed by column, and then by any additional following dimensions. For example, this is what a two dimensional array looks like.
To cement the idea of indexing into multiple dimensions, try implementing printRowAndCol
(which will not be graded).
Then, start by opening MultiArrTest.java
and implementing testMaxValue
. Run the test to ensure it fails.
After writing testMaxValue
, implement maxValue
to return the maximum value found in a multidimensional array. Then, move on to testAllRowSums
followed by allRowSums
. Further details of functionality and examples can be found in the skeleton code.
E. Starting Signpost
For this part of the assignment you will be introduced to Project 0: Signpost. Signpost is a board game that is played on an $N x M$ grid of squares. For this assignment, we are only concerned with a small component of the overall project.
We provided the Place
class, which represents one $(X,Y)$ coordinate on the board. This class has a method called successorCells
, which for every square on the board,
computes all the other squares reachable by a queen move (unlimited movement in a single direction) from that square. In code, directions are numbered 1-8, such that direction d is d * 45 degrees clockwise from straight up (i.e., toward higher y coordinates). Thus, direction 1 is "northeast", 2 is "east", ..., and 8 is "north".
For example, on the above board all reachable squares from (0,0) in direction 8 (north) are (0,1), (0,2), and (0, 3). All reachable squares from (1, 1) in direction 1 (northeast) are (2, 2) and (3, 3).
The provided method is almost fully implemented, but includes two small bugs.
Open this assignment in IntelliJ. Read over the description for this method, run the unit test (which will fail since the code provided is buggy), and observe its output. Once you've read the debugging guide and feel comfortable with how the debugger works, try running through successorCells by setting a breakpoint.
This is code you will use for Project 0, so you are already starting on the project! One of your tasks in Project 0 will be to implement the method successorCells
, which is included here. Once you fixed the two bugs here, feel free to use your solution from this homework on the actual project.
Start by reading through the debugging guide thoroughly!
F. Submission
To get full credit for this assingment, turn in a functioning Arithmetic.java, CompoundInterest.java, and MultiArr.java (each with their approriate testing files!) and the corrected signpost code!
To submit, follow the same instructions as for lab1. Here is a summary of those:
When you're done with the assignment and have properly
committed your changes (git commit -m
...), submit them:
git tag hw1-0 # or hw1-1, hw1-2, for resubmissions
git push
git push --tags
These are required in order to get credit for the homework. In order to recieve full credit your submission must pass 10/15 of the autograder tests on Compound Interest, 3/5 on Multidimensional Arrays, and 1/5 of unit test tests. You must pass all of your own unit tests.