Link

Testing and Debugging Study Guide

Gathering information. The most important part of debugging is gathering information. Information can be gathered in many ways, including by reading the error message, exploring the program in the debugger, drawing visualizations, analyzing the test case, and checking your assumptions.

Hypothesis generation. Using the gathered information, we can incrementally generate better and better hypotheses. Each time we validate a hypothesis, we gain more information about the problem. Information gathered about the system informs productive changes. It is these productive changes that fix bugs.

Why test code? In the real world, chances are you won’t have an autograder. When your code gets deployed into production, it is important that you know that it will work for simple cases as well as strange edge cases.

Test-Driven Development. When provided an autograder, it is very easy to go “autograder happy”. Instead of actually understanding the spec and the requirements for a project, a student may write some base implementation, smash their code against the autograder, fix some parts, and repeat until a test is passed. This process tends to be a bit lengthy and really is not the best use of time. We will introduce a new programming method, Test-Driven Development (TDD), where the programmer writes the tests for a function before the actual function is written. Since unit tests are written before the function is, it becomes much easier to isolate errors in your code. Additionally, writing unit test requires that you have a relatively solid understanding of the task that you are undertaking. A drawback of this method is that it can be fairly slow and it can be easy to forget to test how functions interact with each other.

JUnit tests. JUnit is a package that is used to debug programs in Java. An example function that comes from JUnit is assertEquals(expected, actual). This function checks that expected and actual have the same value. There are a bunch of other JUnit functions such as assertEquals, assertFalse, and assertNotNull. These easiest way to get started with JUnit testing is to have IntelliJ generate the test boilerplate code for you.

Buggy ArrayQueue. We explored two buggy implementations of the ArrayQueue to apply our hypothesis generation and information gathering techniques.

Buggy ArrayQueue1
public class ArrayQueue1<T> {
    private T[] data;
    private int size;
    private int front;
    private int back;

    @SuppressWarnings("unchecked")
    public ArrayQueue1() {
        data = (T[]) new Object[8];
        size = 0;
        front = 7;
        back = 0;
    }

    public void add(T item) {
        data[back] = item;
        back = (back + 1) % data.length;
        size += 1;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    public T remove() {
        T first = data[(front + 1) % data.length];
        front = (front + 1) % data.length;
        data[front] = null;
        size -= 1;
        return first;
    }

    public static void main(String[] args) {
        ArrayQueue1<Integer> queue = new ArrayQueue1<>();
        queue.add(1);
        queue.remove();
        queue.add(3);
        queue.remove();
        queue.remove();
        queue.add(6);
        queue.remove();
        System.out.println("isEmpty() expected true, got " + queue.isEmpty());
    }
}
Buggy ArrayQueue2
public class ArrayQueue2<T> {
    private T[] data;
    private int size;
    private int front;
    private int back;

    @SuppressWarnings("unchecked")
    public ArrayQueue2() {
        data = (T[]) new Object[8];
        size = 0;
        front = 0;
        back = 0;
    }

    public void add(T item) {
        if (isEmpty()) {
            data[back] = item;
            size += 1;
        } else {
            back = (back + 1) % data.length;
            data[back] = item;
            size += 1;
        }
    }

    public boolean isEmpty() {
        return size == 0;
    }

    public int size() {
        return size;
    }

    public T remove() {
        if (isEmpty()) {
            return null;
        }
        T first = data[front];
        data[front] = null;
        front = (front + 1) % data.length;
        size -= 1;
        return first;
    }

    public static void main(String[] args) {
        ArrayQueue2<Integer> queue = new ArrayQueue2<>();
        queue.add(0);
        queue.remove();
        queue.add(2);
        queue.add(3);
        queue.add(5);
        queue.add(6);
        System.out.println("remove() expected 2, got " + queue.remove());
    }
}
  1. In general, is it good to write tests that test your entire program? How about for specific functions?
  2. Write a testing method that will take in 2 arrays and see if they are equal. These arrays can have nested arrays and those nested arrays can have nested arrays and so forth.
  3. If we have 2 objects, Object o1 and Object o2, that have identical qualities, will assertEquals(o1, o2) assert true or false?