Rational: ex07

Exceptions, testing for Exceptions with JUnit

On website: https://ucsb-cs56-pconrad.github.io/tutorials/rational_ex07/

Part of a series of tutorial articles about a Rational class.

Code examples referred to on this page can be found here: https://github.com/UCSB-CS56-pconrad/cs56-rational-ex07

cs56-rational-example/ex07

In ex07, we’ll look at Exceptions in Java by exploring how they can be used to enforce some assumptions we’ve been making about parameters to functions:

About Exceptions

In Java, all exceptions derive from java.lang.Exception, in the following inheritance hierarchy:

There are two main categories of Exceptions:

There are signficant differences between Checked Exceptions and Unchecked Exceptions, in terms of:

A complete discussion of that topic can be found in the Head First Java, 2nd Edition textbook, Chapter 11 (“Risky Behavior”). Exceptions are the focus of that entire chapter.

For this example, we’ll defer a larger discussion of Exceptions. We are dealing only with one particular exception, the exception java.lang.IllegalArgumentException (javadoc here), which happens to be an Unchecked Exception. We can see this in its inheritance hierarchy, which shows that it extends java.lang.RuntimeException:

To throw an exception in Java, we use the java keyword throw, followed by an instance of the the exception we want to throw. It is typical to use an inline constructor invocation to create that instance, as in this example.

   if (denom == 0) {
     throw new IllegalArgumentException("denominator may not be zero");
   }

A few things to note about the example above:

So that’s what the code will look like that throws an exception. But first, let’s write some tests; TDD says “write the test first, and then write the code”. So let’s follow that practice.

About Annotations

To write a JUnit test for an exception, we use annotations. Annotations are those things that start with an at sign (@). We’ve already seen the annotations @Before and @Test in this code from RationalTest.java (note that the … is not part of the code, but indicates some code is omitted.)

  @Before public void setUp() {
    r_5_15 = new Rational(5,15);
    ...
  }

  @Test
  public void test_getNumerator_20_25() {
    assertEquals(4, r_20_25.getNumerator());
  }

These annotations come from the import statements:

import org.junit.Test;
import org.junit.Before;

The javadoc for these annotations can be seen here:

Writing JUnit tests for Exceptions

The easiest way to write a JUnit test for an Exception is to pass a parameter to the @Test annotation, as shown here.

 @Test(expected = IllegalArgumentException.class)
 public void test_denom_zero() {
   Rational r = new Rational(1,0);
 }

This test will pass if an instance of IllegalArgumentException is thrown, and will fail if it is not thrown.

Some notes:

We will add this test to our src/RationalTest.java file, and then test to see the test fail:

test:
    [junit] Testsuite: RationalTest
    [junit] Tests run: 16, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.096 sec
    [junit]
    [junit] Testcase: test_denom_zero(RationalTest):	FAILED
    [junit] Expected exception: java.lang.IllegalArgumentException
    [junit] junit.framework.AssertionFailedError: Expected exception: java.lang.IllegalArgumentException
    [junit]
    [junit]
    [junit] Test RationalTest FAILED				    

We then add this code into our constructor:

   if (denom == 0) {
     throw new IllegalArgumentException("denominator may not be zero");
   }

And we’ll see the test pass:

test:
    [junit] Testsuite: RationalTest
    [junit] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 sec
    [junit]

With that done, we’ll turn our attention to negative numerator and denominator.

Negative num and denom

So, the first thing we do is write some tests that we expect to fail:

   @Test
   public void test_rational_m10_m5() {
     Rational r = new Rational(-10,-5);
     assertEquals("2",r.toString());
   }

    @Test
    public void test_rational_m5_6() {
     Rational r = new Rational(-5,6);
     assertEquals("-5/6",r.toString());
    }

    @Test
    public void test_rational_7_m8() {
      Rational r = new Rational(7,-8);
      assertEquals("-7/8",r.toString());
    }

We run, and whoa! the tests all pass!

So, this leaves us in an interesting position. It appears that the gcd() function may just be taking care of the problem that we anticipated, by factoring out -1, and miraculously, always leaving the negative number in the numerator.

Or, we actually might just have gotten lucky with our choice of tests.

For now, we’ll leave this as an “unsolved mystery”, i.e. do we still have a bug with negative numbers that our tests just haven’t revealed to us? Or can we actually demonstrate, perhaps with a proof, or some other argument, that our code is correct?

Here’s what we can say for sure: if we later discover a bug, the way to proceed is to first add a test that fails because of that bug, i.e.

All the while, retaining all of the other tests we already wrote.

What’s next in Rational: ex08?

Next, we’ll show how to write functions to multiply two rational numbers together.

After that, we’ll take on the task of writing a comparison function, with an eye towards being able to sort collections of rational numbers. That will take us into the land of “common denominators”, which is also something we’ll need if we want to be able to add rational numbers.

On website: https://ucsb-cs56-pconrad.github.io/tutorials/rational_ex07/