Verify the Exception Instance

Each of the exception-based techniques so far essentially tried to infer the identity of the exception that we think should have been thrown by inspecting its characteristics: type, message, cause, and extended attributes. We have used increasing levels of detail to increase our confidence that we have identified the exception properly. You may have asked yourself, “Why not just inject the exception itself? Why not just use identity to verify identity?” That is the question this section addresses.

It almost goes without saying that identity is the strongest way to verify identity. But with objects—and particularly with exceptions—mutability and context are important.

Mutability refers to an object’s ability to be changed. Some languages like C++ support pass-by-value semantics that makes copies of objects when they are passed as arguments. Otherwise, references are passed, and you can change the object through those references.

You may think this is not an issue for exceptions because they are generally immutable; their attributes should not be changeable after their creation or at least should not be changed. You also rarely pass exceptions as arguments, at least in the conventional sense; catch semantics usually pass exceptions by reference, analogous to argument passing by reference.

The most important aspect of exception mutability for our purposes, however, ties into the issue of context. In many languages, the runtime environment automatically inserts a lot of context when it creates an exception. This context differentiates exceptions from other objects significantly. Java includes a stack trace. C# includes several other attributes about the context as well, such as the application name and the throwing method.5

5. Source and TargetSite, respectively.

Unlike a constant of a fundamental data type like int or String, preallocating an exception for injection purposes immutably attaches the context of the object’s creation rather than the context of where it is thrown. I do not recommend using this context to verify the exception. Inspecting the stack trace, for example, couples you very tightly to the implementation. A minor Extract Method [REF] refactoring can lead to Fragile Tests [xTP]. However, the testing frameworks will report the stack trace as part of the failure report in many cases, displaying misleading and ambiguous context for error diagnosis. A shared exception constant will show the same stack trace for multiple throw points: you will see a stack trace that does not actually refer to the place from which the exception was thrown. Additionally, a shared exception constant will show the same stack trace for each throw point when you have multiple test failures.

Now that we have considered the reasons not to use preallocated exceptions, let’s look briefly at an example in which it is arguably justifiable. Listing 11-10 shows a slightly refactored version of our OvenController from earlier.

Listing 11-10: A refactored version of our OvenController

public class OvenController {
  private final int ratedTemperature;
  public OvenController(int ratedTemperature) {
    this.ratedTemperature = ratedTemperature;
  }

  public void setPreHeatTemperature(int temperature)
      throws IllegalArgumentException {
    validateRequestedTemperature(temperature);
    // Set the preheat temperature
  }

  protected void validateRequestedTemperature(int temperature)
      throws IllegalArgumentException {
    if (temperature > ratedTemperature) {
      throw new RequestedTemperatureTooHighException(
          ratedTemperature, temperature);
    }
  }
}

We have refactored the temperature validation logic into a separate protected method, allowing us to inject an exception through an override. Listing 11-11 shows the test for the refactored OvenController.

Listing 11-11: Testing the refactored OvenController with a preallocated exception

@Test
public void testPreHeatTemperature_TooHigh() {
  int ratedTemperature = 600;
  int requestedTemperature = 750;

  IllegalArgumentException expectedException = new
    RequestedTemperatureTooHighException(ratedTemperature,
      requestedTemperature);

  OvenController sut = new FailingOvenController(
      ratedTemperature, expectedException);

  try {
    sut.setPreHeatTemperature(requestedTemperature);
    fail();
  } catch(RequestedTemperatureTooHighException exp) {
    assertSame(exp, expectedException);
  }
}

private class FailingOvenController extends OvenController {
  private final IllegalArgumentException toThrow;
  public FailingOvenController(int ratedTemperature,
      IllegalArgumentException toThrow) {
    super(ratedTemperature);
    this.toThrow = toThrow;
  }

  @Override
  protected void validateRequestedTemperature(int temperature)
      throws IllegalArgumentException {
    throw toThrow;
  }
}

We have used a preallocated exception to drive our result. Note that using it in this way presumes that the validateRequestedTemperature() method is tested separately. Overriding the method as we do here prevents the original method from being invoked. We have avoided the pitfall of using shared exceptions with wildly misleading stack traces by creating a helper class that lets us define the exception very close to where it is thrown and as a nonshared object.

An even more compelling use of this technique would inject an exception that we would expect as the cause of a wrapping exception.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.10.1