Lab 1: Introduction to JUnit testing
Objectives
The objectives of this lab are:
Write JUnit tests to verify that implementation matches the specification.
Writing examples as JUnit tests when only given an interface.
Implement a given simple interface according to specification.
Submit written code to the server and experience the testing feedback.
You are allowed to work with another person on this lab. However both of you must submit individually to the server for it to count towards your grade.
To maximize your learning we recommend that you start by working through the lab by yourself, and work with another person only to brainstorm or debug problems.
If you have not completed Lab 0 yet, start there.
1 Writing examples to explore an interface
One of the core, initial steps in designing any program is understanding
what it is supposed to do. The logical interface of a program is
composed of some implementation details (such as Java class
es and
interface
s) and some written description (such as free-form prose or
Javadoc documentation). It is tempting to skim over the written documentation
and then ask a person —
In CS2500 and CS2510, we taught you to write examples early in the design process, before you’ve implemented your functionality, to build up intuition about what your code is supposed to do. Examples could take the form of comments, or of executable check-expect assertions. They were separate in purpose from writing test cases, which were written after your implementation was complete: test cases ensure the implementation-specific details and fiddly edge-cases of your code are correct.
In the second half of this course, you will be designing the Java
interface
s and class
es that your project needs, based only on
written descriptions. But for the first half of this course, your assignments
will specify the Java interface
s and/or class
es that you will
need to implement.
2 The Fraction
interface
In this section of the lab, you have to implement simple fractions and implement some functionality for them.
A fraction is represented by the Fraction
interface. This interface
should contain the following methods:
A method to add two fraction objects:
Fraction add(Fraction other)
.A method to add a fraction with another given as a numerator and denominator:
Fraction add(int numerator,int denominator)
. This method should throw anIllegalArgumentException
exception if the fraction provided to it is negative.A method that returns the decimal value of a fraction, rounded to the given number of places:
double getDecimalValue(int places)
.
2.1 What to do
Start a new project in IntelliJ. In the src folder, create a package called cs3500.lab1. This is the same as creating the appropriate subdirectory cs3500/lab1 in src in your OS. We will use packages to organize our classes. Note if your lab does not have this subdirectory, the tests on Handins will fail.
Create the above interface in the cs3500.lab1 package, and document its specifications as detailed above.
Implement the
Fraction
interface in aSimpleFraction
class. Leave all the methods blank for now, but document them properly. The specifications for this implementation (beyond what the interface specifies) are:This class can only represent a non-negative fraction. Any attempt to create a negative fraction through the constructor should throw an
IllegalArgumentException
.This class should have a single public constructor that takes the numerator and denominator as integers as its only arguments. Note that these arguments can be individually negative, even as the constructor imposes the above constraint.
This class should also override the
toString
method, which returns a string of the form "n/d". For example, a fraction created with numerator 2 and 4 should return "2/4" through itstoString
method, whereas a fraction created with numerator -4 and denominator -9 should return "4/9" through itstoString
method.Note that wherever applicable, you may not assume that this is the only implementation of the
Fraction
interface.
For each method to be implemented in the
SimpleFraction
class: design and write all JUnit tests to verify its specification, then complete the implementation and run the tests. Follow the directions in Lab 0 to place the test files correctly in your project. For implemetinggetDecimalValue
, you may find the Math class very helpful for rounding and other math operations.When you and a neighbor (who you have not been working with so far) have completed both your test cases and your implementation, email each other your test cases. Add them to your project, and run them. Do they all pass? If not, is the mistake in (a) your implementation, (b) their test case, (c) the specification, or (d) more than one of the above?
Once you have sorted the above out, submit you assignment to the Lab 1 assignment on Handins. To do so, you will need to compress together your src and test folders into a single zip file.
Note: How will you test the constructor? The purpose of the constructor
is to create the object as specified, or die trying (i.e. throw an
exception). The latter can be readily tested (see
Assert.assertThrows
,
but how to test the former? Since we cannot (should not) directly access
fields of the SimpleFunction
object from within the test, the test has
to rely on other (simple) methods to test this. But how do we know those
methods are themselves correct? If these methods are short and simple
(e.g. they do not compute anything, but rather directly report something about
the object) then it is improbable that they work incorrectly. It is a “leap of
faith” in some ways, but it fulfills our objectives of testing everything we
implement, while also not resorting to publicizing fields accessible or writing methods
just to be able to test. In general, our tests will only ever be able to show
that some set of methods is mutually consistent, not that every method
is independently correct.}
3 Submitting to the Handins Server
For your implementation: submit a zip file containing
the code files in your src folder
any test files in your test folder
Please ensure that your submission is a zip file. This zip file should contain your src/ and test/ folders from your project, and only those folders. These folders should mimic the folder structure required for your packages. Please do not put these folders within another folder before submitting, as the grader will not find your files:
A properly-formatted zip file:
A improperly-formatted zip file:
my-submission.zip +-src/ | +-cs3500/ | +-lab1/ | +-Java files for Fractions +-test/ +-all tests you wrote...
my-submission.zip +-My Lab 1/ +-src/ | +-cs3500/ | +-lab1/ | +-Java files for Fractions +-test/ +-all tests you wrote...
When you submit this zip file to Handins, you will be greated with two "graders"
Style checker: This checks your style against the Google style checker as well as some internal style checkers for documentation.
JUnit Autograder: This runs instructor-defined tests against your source code, assuming your code compiles properly. In regular assignments, passing these tests will be part of your grade. In this lab, it is worth 0 points. We provide it in this lab so you are aware how errors on Handins will look.
4 Questions to ponder and discuss
When you are done with the above, here are some additional tasks that you should do. You are encouraged to discuss them with the person next to you, and talk to the course staff as needed.
4.1 Improving the design
The second add
method does not really belong in the interface, although
hopefully you found it useful. A consequence of putting it in the interface is
that it is a public
method in all its implementations.
Can you redesign your classes and interfaces such that this method is no longer public, but other methods still work as described? Whatever design you come up with, what are its limitations?
Implement this design and show to the course staff.
4.2 Better testing
How did you test whether your add
method works correctly? Did you have a
few sample inputs and expected outputs? How many samples did you try? Are they
enough?
An alternative way is fuzzy testing, or random-sample testing. This type of testing is useful when a method can have a large number (seemingly infinite) of inputs that produce expected outputs. It works as follows:
Determine if it is possible to categorize the possible inputs (e.g. all positive numbers, all negative numbers, etc.). This will depend on the specific problem at hand.
For each category, generate a large number of random inputs using a random number generator. Be sure to give a constant seed to the generator, so that one can run the test repeatedly and be assured of the same set of random inputs.
Write a test that repeatedly (e.g. using loops) uses a random input and verifies expected output(s).
How large should the sample size be (i.e. how many sets of random inputs should we try)? Note that this testing probabilistic in nature: we are hoping that we test enough inputs so that the probability of any input producing the correct expected output is "high enough". A few thousand samples are usually a good default size to start with.
Test your add
method using fuzzy testing. Now test the other methods
similarly. Show your work to the course staff.