A. Introduction to JUnit
In lab 1, you saw an example of unit testing, the testing of individual components of a program (methods). To do this, you write extra code that is not actually used in the actual operation of your program, but is instead intended for use during development to find and localize bugs as they happen.
We'll rely on a widely used testing package called JUnit. Your instructor has been quoted as saying "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 lab1, start this homework by using the following commands in your
git fetch shared git merge shared/hw1 git push
You'll receive a hw1 folder with three subdirectories:
IntList. Arithmetic contains a fully implemented
sample program and JUnit test, and the other two folders are programs that
you'll need to implement for this homework.
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
ArithmeticJunitTest.java. In case you're
unfamiliar with the term, the 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
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. Try running the code by compiling with
make (on Unix or
MacOS) and then
java ArithmeticTest. (On Windows, you can compile with
javac instead). You should get the following:
product OK. sum FAILS.
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
assertEqualsthat perform some simple test 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
specified classes at execution time and call all of the methods in
them that are annotated to be tests,
i.e. those methods with
@Testproceeding their definition.
As an example, look at the
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,
- Some of the methods have
@Testright 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 for each such method found, executes that method.
- The main method performs the mysterious task
System.exit(ucb.junit.textui.runClasses(ArithmeticJUnitTest.class)). In English, this just means that every method in the class named
ArithmeticJUnitTestthat 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.
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. Compound Interest
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
Your goal in this problem is to simply 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 starter test, we can use:
$ make check
which (as you can see from
Makefile) simply runs the command
after first making sure that
CompoundInterestTest.class is up to date.
Those of you on Windows can simply use this command.
This will compile the starter code and run the JUnit test, and 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.
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 Part 1, please give the test-first approach a fair shake. We believe it will save you grief in the future.
Start this homework by opening
CompoundInterest.java in your text editor of choice. In
CompoundInterestTest.java, you'll see a bunch of tests you're supposed to
as a guide, edit the
method so that it acts as a good test of whether or not
the specifications given in the documentation comments in
assertEquals statements are probably good
enough. We're throwing you right in 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.).
numYears returns an int, 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 the
make check command, 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 bunches of tests that don't fail out of the box.
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.
Repeat the exercise from before, but now with the
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.
Now we'll write a method that computes the future value of an appreciating (or depreciating) asset if we take into account inflation. Having a million dollars today is very different than 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 will be worth in 40 years and we assume the inflation rate will be 3 percent over the next 40 years, we'd see they'd be worth $1,000,000 * (0.97)^(40) or $295,712.28 in 2014 dollars. Not bad, but not quite so impressive.
Again, start by writing the tests, then use
make check to ensure that
the tests correct 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 everytime 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.
In case you're curious about the parametric assumptions made in main, see the end of this assignment for more.
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
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. If you want, you can
check your answer using this web
this web tool (make sure to click end of year and set the investment
period to 41 years).
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.
IntList folder, you'll see an implementation of the
as discussed in class during lecture 4. Your task for this problem is to
add four new methods for manipulating IntLists:
dsublist. The desired behavior for these functions is
given in the starter code for IntList.
Start by implementing
testDcatenate in IntListTest.java. Run
check to ensure your test fails (since you haven't implemented
testDcatenate, move on to the
dcatenate method in
IntListTest. You're welcome to use either iterative or recursive code in
your solution, but we recommend a recursive approach.
Once you've completed
dcatenate, repeat this process
for the next three methods of IntList.
When you're done with all four tests and
IntList methods, and have properly
committed them (
git commit -a ...), submit them as for
git tag hw1-0 # or hw1-1, hw1-2, for resubmissions git push git push --tags
This is required in order to get credit for the homework.
D. Regarding Investment Assumptions
We were seemingly quite optimistic in our assumptions, expecting a 10% yearly return and a 3% inflation rate. This rate of return was assuming that you've invested your money in what is known as an index fund. Index funds have become particularly popular in recent years, and for good reason. Over the past half century, an investment portfolia that simply consists of every single company in the S&P 500 would have earned an effective interest rate of 10% per year (the actual returns are highly volatile, but averaged over a very long period of time it comes to about 10%).
This seemingly naive (and easy to automate!) strategy of simply investing in a large number of large companies actually does quite well. In contrast, there are also managed funds, which are steered by analysts who try to beat the market, i.e. try to do better than the naive strategy of investing in the entire S&P (or similar). Intriguingly, despite their highly capable employees, such managed funds have traditionally been unable to beat managed funds in performance (see this, for example). And analysts don't work for free. If you invest in managed funds, the company running it will take a cut that ends up making a huge difference in your overall investment payoff.
Caveat emptor: While the S&P 500 has historically done quite well, the stock markets of other nations have not been quite so lucky. It is entirely possible that we are experiencing a phase shift in the American (and perhaps global) economy; those sweet 10% returns might evaporate for good, and who knows what sort of financial dystopia we might find outselves in.