Search code examples
javajunitthrowable

What is the correct way to compare two throwables?


I am using a JUnit Rule to immediately re-run any failed tests. My extra requirement is, if the re-run also fails, determine whether they failed for the same reason.

To do this I've adapted the code from this answer to keep a record of the failures and compare them. However, the comparison (.equals) always evaluates to false despite them failing for the same reason. What is the best way to go about this?

  private Statement statement(final Statement base, final Description description) {
    return new Statement() {
      @Override
      public void evaluate() throws Throwable {

        for (int i = 0; i < retryCount; i++) {
          try {
            base.evaluate();
            return;
          } catch (Throwable t) {
            System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed");

            // Compare this error with the one before it.
            if (errors.size() > 0) {
              if (t.equals(errors.get(errors.size() - 1))) {
                System.out.println("The error is the same as the previous one!");
              } else {
                System.out.println("The error is different from the previous one.");
              }
            }

            errors.add(t);
          }
        }
        System.err.println(description.getDisplayName() + ": giving up after " + retryCount
            + " failures");

        // Throw most recent error.
        throw errors.get(errors.size() - 1);
      }
    };
  }

Solution

  • Equality for a Throwable is defined as "the same Throwable". (It compares references.)


    "Same reason" is something you need to think about and define for your application.

    One way we could define it is "the same type and loosely the same throw statement":

    static boolean sameTypeAndLine(Throwable t1, Throwable t2) {
        if (t1.getClass() == t2.getClass()) {
            StackTraceElement[] trace1 = t1.getStackTrace();
            StackTraceElement[] trace2 = t2.getStackTrace();
            return trace1[0].equals(trace2[0]);
        } else {
            return false;
        }
    }
    

    But that still has ambiguity:

    • if (bad1 || bad2) {
          // same throw site, different conditions
          throw new Exception(...);
      }
      
    • // throws NullPointerExeption
      // (was it foo or the result of bar() that was null?)
      Object baz = foo.bar().baz();
      

    So, the best thing you can do is clearly define reasons for your exceptions:

    class MyException {
        final Reason reason;
    
        MyException(Reason reason) {
            this.reason = reason;
        }
    
        // or a class, or whatever you need
        enum Reason {A, B, C}
    }
    

    And check against those. Then additionally, use a coding style which prevents ambiguities.