Lab 1: Introduction to JUnit testing
Objectives
1 Writing examples to explore an interface
1.1 Examplar 1:   sorting strings
1.2 Examplar 2:   sorting, generically
2 The Fraction interface
2.1 What to do
3 Questions to ponder and discuss
3.1 Improving the design
3.2 Better testing
8.9

Lab 1: Introduction to JUnit testing

Objectives

The objectives of this lab are:

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 classes and interfaces) 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 — a colleague, internet help forums, etc — for more detail. But doing so too quickly prevents you from developing your own insights into what should be happening.

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 interfaces and classes that your project needs, based only on written descriptions. But for the first half of this course, your assignments will specify the Java interfaces and/or classes that you will need to implement. And this gives you a new and different oppportunity to build your understanding of an assignment, by working with Examplar.

1.1 Examplar 1: sorting strings

Consider a utilty class that provides a method to sort a list of strings by a comparator:

package cs3500.lab1;

public class SortUtils {
  /**
   * Produces a new list containing the elements of values, in sorted order.
   */
  public static List<String> sortStrings(List<String> values, Comparator<String> compare) {
    throw new RuntimeException("Not yet implemented");
  }
}

You know several algorithms for implementing this method; there are even built-in implementations of it in Java already. The purpose statement of the method is given above, and likely looks similar to purpose statements you may have written for your own methods. This purpose statement is badly flawed, in that there is a wide gap between what this statement permits and what our intuition for a sorting method might be.

Once you have a few scenarios discussed, it’s time to try to see which of them were intended by the authors of SortUtils, and which were unintended mistakes.

1.2 Examplar 2: sorting, generically

The authors of the SortUtils class recognized that sorting can be useful beyond just for strings, and so developed a second method:

package cs3500.lab1;

public class SortUtils {
  /**
   * Produces a new list containing the elements of values, in sorted order.
   * @param <T> the type of elements to sort
   */
  public static <T> List<T> sort(List<T> values, Comparator<? super T> comp) {
    throw new RuntimeException("Not yet implemented");
  }
}

Hint: for the particular chaffs given to you here, the bugs in sort are a strict subset of the bugs in sortStrings.

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:

2.1 What to do

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 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.

3.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.

3.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:

  1. 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.

  2. 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.

  3. 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.