Unit tests are tests meant to verify the external behavior of some program. More precisely, a good suite of tests should decisively answer the following question: “Does some program X match the expected behavior of its specification Y?” Not only that, you should be testing to make sure your program breaks in the correct ways as well. If somebody tries using your program in an invalid way, does it crash correctly?

  • The happy cases: Test how your clients will normally use your code.
  • Errors and exceptions: Do you throw the correct exceptions when bad input is given? Did you forget to implement a certain exception? Do you check for an error condition too late?
  • Preconditions and postconditions: Try checking/stress-testing your preconditions. Are your postconditions always satisfied afterwards?
  • Empty and null cases: What happens when you pass in zero? An empty string or list? A null?
  • Edge cases: Test for edge cases and off-by-one errors. Look for “boundaries” in your input data.

    What is a boundary?

    As an example, suppose you’re writing a program for a bar to use to check if someone is old enough (21+) to buy alcohol. If they are under 21, it should print a particular message saying they aren’t old enough, otherwise it should welcome them. Here is a (potentially buggy) solution we came up with.

    public static void canServe(int age) {
        if (age <= 21) {
            System.out.println("Not old enough, come back in "
                + (21 - age) + " years");
        } else {
            System.out.println("Welcome in! What do you want?");
        }
    }
    

    With our knowledge of the specification, we can think of this number 21 as a “boundary” between how the program should behave one way vs. another. With our knowledge of our implementation, we know our conditional is doing something else around 21. So knowing these “boundaries” help us come up with good test cases.

    If we test the function with the input 21, we would see it prints the wrong message because we have a <= instead a <.

    Testing near the boundary can also be a good idea, making sure it behaves well with inputs of 20 and 22.

    Another strategy is to try generating randomized data to hunt for weird edge cases.

  • Mixing multiple methods: Each method individually might work fine, but what happens if you try using multiple of them, called in all kinds of different orders?