Оператор переключения покрытия кода JUnit с веткой по умолчанию

У меня есть метод util для обработки текста. Способ обработки определяется enum Mode. При написании модульного теста для покрытия этого метода мне пришлось добавить фиктивный элемент enum, чтобы включить default часть оператора switch в тестирование. Часть по умолчанию — это моя проверка на случай, если enum расширен новым элементом, и этот новый элемент не добавлен в оператор switch, что означает, что обработка для этого нового элемента будет невозможна. Если я удалю фиктивный элемент и попробую использовать null, то получу NPE, а Java даже не введет оператор switch — это нормальное поведение, когда вы используете null для оператора switch.

Вопрос: есть ли способ избежать добавления фиктивного enum элемента, но при этом покрыть default часть оператора switch в модульном тесте?

Дополнение для ясности: Java 17, JUnit Jupiter 5, Инструмент покрытия кода Eclipse.

Код

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"
                );
        }
    }

}

Модульный тест

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

Модульный тест без дополнительного фиктивного элемента перечисления

Покрытие кода без дополнительного фиктивного элемента перечисления


Юнит-тест с дополнительным фиктивным элементом перечисления

Покрытие кода без дополнительного фиктивного элемента перечисления

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
113
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вам не нужно создавать или расширять существующее enum Mode. Вам просто нужно высмеять это перечисление с помощью Mockito.
Для этого вы можете использовать Mockito версии 5 или более поздней версии, которая поддерживает Mockito-inline в качестве средства создания макетов по умолчанию. Это позволит легко поддерживать издевательский статический метод.

Группа testImplementation: «org.mockito», имя: «mockito-core», версия: «5.11.0»

Стратегия состоит в том, чтобы создать макет перечисления UNSUPPORTED. Используйте Mockito.mockStatic, чтобы имитировать статический метод values() перечисления и возвращайте существующие значения плюс фиктивное перечисление UNSUPPORTED.
Тогда это псевдо-перечисление UNSUPPORTED вызовет путь по умолчанию для вашего оператора switch-case, что позволит вам легко выполнить утверждение.

Фрагмент кода


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());
        }
    }
}

Оно работает! Мне пришлось удалить часть сообщения об исключении, потому что name() метод UNSUPPORTED вызвал ArrayOutOfBoundsException, но я могу с этим жить - все равно в 1000 раз лучше, чем вводить бесполезный фиктивный enum элемент только для того, чтобы соответствовать какому-то инструменту покрытия кода.

horvoje 17.04.2024 19:20

При необходимости вы также можете имитировать возврат метода name() из мока UNSUPPORTED, как и функцию ordinal().

ThangLeQuoc 18.04.2024 03:50

Нет, используйте JDK. Java 17 позволяет переключаться с помощью перечислений и проверяет, все ли случаи учтены. В противном случае код не будет компилироваться, поэтому вам даже не понадобится тест (см. другой ответ на этот вопрос).

M. Deinum 18.04.2024 09:22

Поскольку вы используете Java 17 и ваши ветки возвращают значение, вместо этого вы можете использовать выражение переключения , которое должно быть исчерпывающим (поэтому компилятор проверит, что вы охватываете все случаи).

    public static String process(
            final String text,
            final LineBreaker.Mode mode,
            final int amount
    ) {
        String result = switch (mode) {
            case LAST_SPACES -> "some processing result";
            case LAST_CHARS -> "another result";
        };

        return result;
    }

Таким образом, вам не понадобится ветка по умолчанию или тест.

Идея default состоит в том, чтобы принудительно реализовать все элементы перечисления и покрыть недостающие реализации исключениями. Например, если добавляется еще один элемент (назовем его FIRST_SPACES), то я хочу, чтобы он умер, выдав исключение, а не получил неожиданный результат. Я не уверен на 100%, что это лучшая архитектура, но пока у меня есть хороший опыт работы с ней. Это довольно простое перечисление, состоящее всего из двух элементов, но в некоторых других случаях, когда присутствует больше элементов, я думаю, что хорошо иметь такую ​​«защиту».

horvoje 18.04.2024 18:10

Другие вопросы по теме