Search code examples
javaunit-testingjunitcode-coverage

JUnit code coverage switch statement with default branch


I have util method to process some text. The way of processing is determined by enum Mode. While writing unit test to cover this method, I had to add dummy enum element in order to include default part of switch statement into testing. Default part is my check for the case if enum is extended by new element and this new element is not added to switch statement meaning processing would be impossible for that new element. If I remove dummy element and try with null, then I get NPE and Java don't even enter switch statement - which is normal behavior when you use null for switch statement.

The question: Is there a way to avoid adding dummy enum element but still cover default part of switch statement in unit test?

Addition for clarity: Java 17, JUnit Jupiter 5, Eclipse Code Coverage Tool

Code

abstract class LineBreaker {

    public enum Mode {
        LAST_SPACES,
        LAST_CHARS,
        DUMMY
    }

    public static String process(
        final String text,
        final LineBreaker.Mode mode,
        final int amount
    ) {
        switch (mode) {
            case LAST_SPACES : {
                // blah blah create `processingResult`
                return processingResult;
            }
            case LAST_CHARS : {
                // blah blah create `processingResult`
                return processingResult;
            }
            default :
                throw new IllegalStateException(
                    "Processor for " +
                        LineBreaker.class.getSimpleName() + "." +
                        LineBreaker.Mode.class.getSimpleName() + "." +
                        mode.name() +
                        " not implemented"
                );
        }
    }

}

Unit test

@Test
void testLineBreakerException() {
    assertThrows(
        IllegalStateException.class,
        () -> LineBreaker.process( "Dog", LineBreaker.Mode.DUMMY, 10 ),
        "Expected " + IllegalStateException.class.getSimpleName()
    );
}

Unit test without additional dummy enum element

Unit test without additional dummy enum element

Code coverage without additional dummy enum element

Code coverage without additional dummy enum element


Unit test with additional dummy enum element

Unit test with additional dummy enum element

Code coverage without additional dummy enum element

Code coverage without additional dummy enum element


Solution

  • You don't need to create or extend the existing enum Mode. You just need to mock this enum with Mockito.
    For this, you can use Mockito version 5 or later, which support mockito-inline as the default mockmaker. This will support mocking static method easily.

    testImplementation group: 'org.mockito', name: 'mockito-core', version: '5.11.0'

    The strategy is to create a mock enum UNSUPPORTED. Use Mockito.mockStatic to mock the static method values() of the enum, and return the existing values plus the mock enum UNSUPPORTED.
    Then this mock UNSUPPORTED enum would trigger the default path of your switch-case statement, which allow you to perform the assertion easily.

    Code snippet

    
    import org.junit.jupiter.api.Test;
    import org.mockito.MockedStatic;
    import org.mockito.Mockito;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertThrows;
    import static org.mockito.Mockito.doReturn;
    
    public class SOEnumQuestionTest {
    
        public enum Mode {
            LAST_SPACES,
            LAST_CHARS
        }
    
        public static String process(final Mode mode) {
            switch (mode) {
                case LAST_SPACES : {
                    return "processing last spaces";
                }
                case LAST_CHARS : {
                    return "processing last chars";
                }
                default :
                    throw new IllegalStateException("Processor not implemented");
            }
        }
    
        @Test
        public void testProcessWithDefaultSwitchCase() {
            try (MockedStatic<Mode> dummyModeMockedStatic = Mockito.mockStatic(Mode.class)) {
                final Mode UNSUPPORTED = Mockito.mock(Mode.class);
                // We want to give the mocked enum in the last position of the enum values list
                // E.g: Enum has 2 values -> our mock UNSUPPORTED enum index is 2
                doReturn(2).when(UNSUPPORTED).ordinal();
                dummyModeMockedStatic.when(Mode::values)
                        .thenReturn(new Mode[]{
                                Mode.LAST_SPACES,
                                Mode.LAST_CHARS,
                                UNSUPPORTED});
    
                Throwable expectedException = assertThrows(IllegalStateException.class, () -> SOEnumQuestionTest.process(UNSUPPORTED));
                assertEquals("Processor not implemented", expectedException.getMessage());
            }
        }
    }