Как я могу проверить недопустимый тип перечисления в случае переключателя?

Я столкнулся с проблемой, когда все значения перечисления реализованы в операторе переключения, и я не могу принудительно создать исключение UnsupportedOperationException. Я прочитал несколько статей, но мне ничего не помогло

Вот метод, который я хочу протестировать:

public ResponsePaginatedDto execute(DeviceType type) {
    return switch (type) {
        case APP -> handleApp();
        case WEB -> handleWeb();
        case DESKTOP -> handleDesktop();
        default -> throw new UnsupportedOperationException();
    };
}

И вот мое перечисление:

public enum DeviceType {
    APP,
    WEB,
    DESKTOP;
}

Что я пробовал:

  • Использование PowerMockito: я попытался имитировать значение перечисления с помощью PowerMockito, чтобы имитировать неподдерживаемое значение, но мне не удалось заставить его работать должным образом.
  • Создание копии перечисления в тестовом пакете. Я пытался создать копию перечисления DeviceType в тестовом пакете, чтобы ввести неподдерживаемое значение, но этот подход тоже не сработал, поскольку приводил к конфликтам и не ощущался как чистое решение.
  • Введение недопустимого значения перечисления. Я рассматривал возможность добавления недопустимого значения перечисления только в целях тестирования, но считаю, что это не очень хорошая практика, поскольку это приведет к искусственному увеличению перечисления исключительно для тестирования, что может привести к путанице и проблемам с обслуживанием в будущее.

Чего я ожидал: Я ожидал найти способ протестировать исключение UnsupportedOperationException без необходимости изменять исходное перечисление или вводить искусственные значения. Моей целью было найти метод, который позволил бы мне смоделировать неподдерживаемое значение перечисления и вызвать исключение чистым и удобным в обслуживании способом.

Почему вы хотите это проверить? Я согласен с высоким покрытием тестов и всем остальным, но компилятор уже будет жаловаться (по крайней мере, предупреждать), если оператор switch не покрывает все значения enum и не имеет значения по умолчанию. Возможно, вам просто нужно провернуть предупреждения компилятора.

Robert 14.08.2024 21:53

Невозможно сделать то, что вы хотите, без хакерства (без лекарства, которое значительно хуже самой болезни), @EduardoLopes. «100%-ное покрытие тестами» — это чушь, написанная людьми, которые не программируют и просто жалуются в постах в блогах. Этот последний 1% — это сок, который не стоит выжимать. Если хотите, вы можете обойти это: включите предупреждения компилятора, чтобы отсутствующие случаи вызывали предупреждения, а затем удалите случай default. Меньше кода для загрузки, победа-победа. 100%-ное покрытие тестами обычно означает, что вам необходимо полностью удалить весь защитный/резервный код, что на самом деле не очень хорошо. Таким образом: 100% цель теста плохая.

rzwitserloot 14.08.2024 22:18

@Eduardo - Кстати, компилятор всегда должен выдавать ОШИБКУ, если какой-то регистр отсутствует и не указано предложение по умолчанию, нет необходимости запускать предупреждения компилятора (JLS 15.28.1. Блок переключателя выражения переключателя: «Это ошибка времени компиляции, если выражение переключателя не является исчерпывающим", вроде понятно, поскольку выражение должно всегда возвращать значение или аварийно завершаться) - но, к сожалению, этого не всегда достаточно...

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

Ответы 2

Я нашел лучшее решение, удалив регистр по умолчанию из оператора переключателя. Если вы опустите регистр по умолчанию и переключатель не охватывает все возможные значения перечисления, компилятор выдаст ошибку: «Выражение Switch должно охватывать все возможные значения Java(1073743533)». Такой подход гарантирует, что если в будущем будет добавлено новое значение перечисления, код не будет компилироваться до тех пор, пока новое значение не будет явно обработано, что делает его более надежным и удобным в обслуживании решением.

Вот как я это сделал:

Оригинальный метод:

public ResponsePaginatedDto execute(DeviceType type) {
    return switch (type) {
        case APP -> handleApp();
        case WEB -> handleWeb();
        case DESKTOP -> handleDesktop();
        default -> throw new UnsupportedOperationException();
    };
}

Модифицированный метод:

public ResponsePaginatedDto execute(DeviceType type) {
    return switch (type) {
        case APP -> handleApp();
        case WEB -> handleWeb();
        case DESKTOP -> handleDesktop();
    };
}

«Доктор, доктор: у пациента отсутствует мизинец левой ноги!». Врач: :Берет дробовик и стреляет в пациента:. Врач: «Вот. Все решено». Это ты. Ты тот доктор. Что ты делаешь?

rzwitserloot 14.08.2024 22:52

хахахаха, я решил использовать другой подход и вместо этого удалил регистр по умолчанию из оператора переключателя. Таким образом, код не сможет скомпилироваться, если в будущем будет добавлено новое значение перечисления без его явной обработки, что гарантирует учет всех возможных случаев. Я обновлю свой ответ

Eduardo Lopes 14.08.2024 22:58

К вашему сведению, компилятор добавляет default -> throw new MatchException(null, null); или что-то подобное в переключатель второго кода.

user85421 14.08.2024 23:07
Ответ принят как подходящий

Этот конкретный пример - исправление

Блок switch здесь используется как выражение. Это означает, что вы просто полностью удаляете блок default:

  • Ваш код скомпилируется нормально.
  • Когда вы добавляете новое значение в это перечисление, вы получаете красную волнистую подчеркивание в этом блоке переключателей, что значительно лучше, чем «все компилируется и работает, казалось бы, нормально, пока код не взорвется с UnsupportedOperationException без сообщения».
  • Если каким-то образом вам удастся запустить этот код, добавив при этом перечисление (что требует, чтобы вы скомпилировали исходный файл с этим ключом в момент времени X и перекомпилировали исходный файл, содержащий перечисление, в момент времени Y, где X предшествует Y, и в X перечисление имело только 3 значения, которые оно имеет сейчас, тогда как в Y вы добавили значение - маловероятный/невозможный сценарий, если вы очистите сборку перед развертыванием, что обычно и нужно), вы... получите исключение. Не UnsupportedOperationException, а java.lang.MatchException, но тем не менее исключение.

Это решит все ваши проблемы, если вы готовы принять MatchException вместо UnsupportedOperationException. Это даже намного лучше: теперь вы можете получить 100%-ное покрытие тестами и получать предупреждения во время компиляции, если вам не удается охватить перечисление, вместо туманной ситуации «надеюсь, что когда-нибудь произойдет какая-то ошибка», и меньше печатать и читать Код переключателя больше не предполагает ошибочного предположения, что 3 значения перечисления, для которых вы написали case, не охватывают все это и что есть 4-е или 5-е. Ведь если не было, зачем писать default?

Этот конкретный пример — плохой код

100% тестовое покрытие — это глупая загвоздка. Это не значит, что ваш код великолепен. Например, здесь вы написали довольно плохой код. Если когда-либо происходит невозможное и выдается UnsupportedOperationException, то смысл сообщения об ошибке, являющегося частью исключения, заключается в передаче полезной информации, имеющей отношение к данному случаю. «Параметры, которые привели к ошибке», более или менее. Здесь, конечно, вы передаете ценность! Так и должно быть throw new UnsupportedOperationException(type.name()) - то что нет - это баг, прям. Однако это ошибка, которую вы не можете проверить. Видеть? Сам ваш фрагмент доказывает, что стремление к 100%-ному покрытию тестами — это странная вещь. Это не дает вам какой-то хваленой награды «ошибки вообще невозможны», и этот последний 1% требует, чтобы вы полностью удалили защитный/резервный код, потому что вы не можете сделать это, не разрушая свою кодовую базу и фактически увеличивая шансы. этой проблемы, добавляя способы создания невозможного состояния (хотя бы только для того, чтобы ваши тесты могли охватить невозможное состояние). 1

Для всех таких сценариев не существует хорошего решения!

Здесь мы смогли «исправить» проблему и предоставить вам код, который был бы лучше, чем у вас, а также получить другие приятные преимущества, а также обеспечить 100% тестовое покрытие.

Но не всегда все так хорошо: иногда резервная/защитная строка кода весьма полезна в качестве защиты, но тогда вы сталкиваетесь с дилеммой. Или:

  • Вы не можете покрыть кодом эту строку, потому что она буквально сканирует то, чего не может произойти. Часто это происходит в более сложном коде, где ваш мозг и навыки анализа приводят к тому, что вы не можете понять, как может возникнуть ситуация X. Вероятно, это потому, что X на самом деле не может произойти, но защитные линии все еще полезны (см. ниже), или

  • На самом деле вы добавляете код и значительно усложняете его отслеживание, намеренно создавая опоры исключительно для целей ваших тестовых случаев: вы позволяете создать эту непостижимую ситуацию только для того, чтобы вы могли ее протестировать. Звучит здорово (дааа, 100% тестовое покрытие, балл!), но не так быстро. Этот код почти наверняка намного хуже, чем код без всего этого странного круфта, который вместо этого набирает 99%. Видя в коде способ создания целого ряда новых состояний, вам становится сложнее рассуждать об этом, и, что также важно, автоматическим инструментам это сделать становится невозможным. Тот факт, что способы создания этих новых состояний, которые трудно даже объяснить (поскольку они не моделируют ничего из реальной жизни - вы добавили их исключительно для проверки защитных/запасных вариантов), не помогает. Это настоящий «доктор, мне больно, когда я бью молотком по лицу». Ответ: тогда перестаньте это делать.

100% покрытие кода — это инструмент. Если использование инструмента требует, чтобы ваш код стал плохим и его было труднее рассуждать, правильный образ действий — прийти к выводу, что инструмент плох в том сценарии, в котором вы оказались. Не прогибаться назад и умирать на этом холме.

Третий способ — просто удалить все резервные варианты. Любой код, который пытается сработать в сценарии, который вы не можете понять, вместо этого может быть удален. Однако это очень плохая идея. Не только потому, что предполагается, что вы всеведущи. А еще потому, что резервные варианты защищают от неправильной интерпретации того, как написанный код будет использоваться будущими программистами.

Например, предположим, что у меня есть метод, который возвращает ширину прямоугольника, и у меня есть другой код, который принимает прямоугольник и выполняет некоторые математические вычисления по его длине. Вы не можете понять, что вообще означает отрицательная ширина прямоугольника, поэтому решаете написать что-то вроде:

if (rectangle.getWidth() < 0) throw new IllegalArgumentException("Negative width rectangles not allowed");

Это хорошая вещь. Даже если сегодня невозможно создать прямоугольники с отрицательной шириной, возможно, завтра какая-нибудь предприимчивая душа добавит это в систему и не учтет, что код, в котором это есть if, необходимо обновить, чтобы справиться с этим. Прежде чем продолжить: «Отрицательная ширина? Что за чушь, такая нелепая вещь никогда не может случиться» — долгое время математики думали так же, пытаясь извлечь квадратный корень из отрицательного числа, пока кто-то не сказал: «Давайте просто… определим некоторую константу как значение». для sqrt минус 1 и посмотрим, что произойдет.

Вы не знаете того, чего не знаете, и не можете объяснить необъяснимое.

Следовательно, защитные/запасные линии — необходимая вещь в кодировании, они могут быть хорошей идеей. И они непроверяемы.

Помните свое лицо и молот?

Перестаньте его размахивать.


[1] К сожалению, MatchException также не печатает непокрытое значение. Это плохо, и OpenJDK должен это исправить. Однако это может дать вам возможность распечатать непокрытое значение в более поздней версии. Это не просто несбыточная мечта: это случилось с NullPointerException: на современных JVM, если NPE вызван разыменованием (вместо new NullPointerException(...)), сообщение сгенерированного NPE будет включать текстовое представление выражения, которое было null если это достаточно просто сделать. Это означает, что если бы вы только что оставили это значение на return person.getName(), вы бы получили NPE с текстом person, тогда как если бы вы явно написали if (person == null) throw new NullPointerException();, вы этого не получите. «Ленивый» программист побеждает.

NIT: константа (i) определяется не как sqrt из -1, а как i² = -1 ;)

knittl 14.08.2024 22:58

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