Testabilty of Enum Switch Statements

Let’s start with an easy one. Or so you might think.

The Easy One

How to perform conditional actions based on an enum value in Java with 100% unit test branch coverage?

Assuming our enum is declared in MyEnum.java:

public enum MyEnum {
  A,
  B,
  C
}

The corresponding switch statement in MyAction.java would be:

public class MyAction {
  public void doIt(MyEnum myEnum) {
    switch(myEnum) {
    case A:
      doA();
      break;
    case B:
      doB();
      break;
    case C:
      doC();
      break;
    }
  }

  // Implementation of doA, doB, and doC...
}

For the sake of this example, we keep the junit test in MyActionValuesTest.java simple and just care about coverage:

// This is not a sufficient unit test!
// Always test your enum values individually!

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class MyActionValuesTest {
  @Parameters
  public static MyEnum[] data() {
    return MyEnum.values();
  }

  @Parameter
  public MyEnum testEnum;

  @Test
  public void testDoIt() {
    new MyAction().doIt(testEnum);
    // Add some assertions in your real tests!
  }
}

The parameterized junit test gets invoked with each value from the static data() method.

Done.
That was easy.

But wait… why is the unit test branch coverage below 100%?

Jacoco Report

We tested all enum values, what’s missing?
Go back to the MyAction.java and take a close look!

The default Label

That’s right, the switch block is missing the default label.
But why do we need a default label when all enum values are covered?
Because MyEnum might be extended in the future (possibly residing outside your project) without MyAction.java being recompiled (see this stackoverflow answer on the necessity of default labels for details).

Would it help to implement per-constant methods in the enum declaration (see the Java 1.5 tutorial back when Enums were introduced)? No, because we want to decouple the bussiness logic from the data type.

Or should we use if-then-else statements instead of the switch block? No, because static code analyzers can detect if almost all enum values are covered in a switch block and only one is missing (see Eclipse help). This cannot be detected for if-then-else statements.
Futhermore, the default label helps other developers locating the end of the switch block and it indicates that you have considered all possibilities.
Hence, let’s add the default label in MyAction.java (version 2):

public class MyAction {
  public void doIt(MyEnum myEnum) {
    switch(myEnum) {
    case A:
      doA();
      break;
    case B:
      doB();
      break;
    case C:
      doC();
      break;
    default:
      throw new IllegalArgumentException(
          "unhandled enum value: " + myEnum);
    }
  }

  // Implementation of doA, doB, and doC...
}

Since normal execution should never reach the default label, an IllegalArgumentException is thrown 1.

Testing the Impossible

But how can we test for impossible enum values?
PowerMock can mock final static methods, which allows us to extend the MyEnum.values() method with a new enum value. Here is our MyActionInvalidValueTest.java test (blatantly copied from this stackoverflow answer on mocking enums):

import java.util.Arrays;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.powermock.reflect.Whitebox;

@RunWith(PowerMockRunner.class)
public class MyActionInvalidValueTest {
  @Test(expected = IllegalArgumentException.class)
  @PrepareForTest(MyEnum.class)
  public void testInvalidValue() throws Exception {
    MyEnum[] origValues = MyEnum.values();

    MyEnum invalidValue = PowerMockito.mock(MyEnum.class);
    Whitebox.setInternalState(invalidValue, "name", "-");
    Whitebox.setInternalState(invalidValue, "ordinal", origValues.length);
    MyEnum[] newValues = Arrays.copyOf(origValues, origValues.length+1);
    newValues[newValues.length-1] = invalidValue;

    PowerMockito.mockStatic(MyEnum.class);
    PowerMockito.when(MyEnum.values()).thenReturn(newValues);

    new MyAction().doIt(invalidValue);
  }
}

With this additional test, our unit tests achieve 100% branch coverage.

Benefits

But is the branch coverage worth the hassle? And do we need the parameterized unit test in MyActionValuesTest.java?
It is if MyEnum might be extended and the code base contains multiple such switch block in different locations.

  • The MyActionInvalidValueTest ensures that an exception is thrown for unhandled values (read in this issue of the Java Specialist newsletter why this is so important).
  • The MyActionValuesTest will fail accordingly when MyEnum gets extended without adjusting the switch block.

Note: Although we tested for all possible and impossible enum values, we did not test for null. Be aware that null causes the switch statement to throw a NullPointerException.
Of course, never forget to write unit tests for the individual enum values, ensuring that doA(), doB(), and doC() are called accordingly!


  1. The Java 1.5 tutorial suggested throwing an AssertionError. However, an Error denotes a fatal problem and is not meant to be caught. An IllegalArgumentException appears more appropriate, because an outdated version of MyAction might be called by another project, which is aware of the altered MyEnum and catches the Exception gracefully. 
Advertisements