Неплохо ли использовать рефлексию в модульном тестировании?
В последние годы я всегда думал, что в Java Reflection широко используется во время модульного тестирования. Поскольку некоторые из переменных/методов, которые необходимо проверить, являются частными, необходимо как-то прочитать их значения. Я всегда думал, что API Reflection также используется для этой цели.
На прошлой неделе мне пришлось протестировать некоторые пакеты и, следовательно, написать некоторые тесты JUnit. Как всегда, я использовал Reflection для доступа к частным полям и методам. Но мой руководитель, который проверил код, был не очень доволен этим и сказал, что API Reflection не предназначен для использования для такого "взлома". Вместо этого он предложил модифицировать видимость в производственном коде.
Неужели плохо использовать практику Reflection? Я не могу поверить в это -
Изменить: я должен был упомянуть, что мне требовалось, чтобы все тесты были в отдельном пакете под названием test (поэтому использование защищенного visibilty, к примеру, тоже не было возможным решением)
Ответы
Ответ 1
IMHO Reflection действительно должно быть только последним, зарезервированным для специального случая устаревшего кода модульного тестирования или API, который вы не можете изменить. Если вы тестируете свой собственный код, тот факт, что вам нужно использовать Reflection, означает, что ваш дизайн не тестируется, поэтому вы должны исправить это, вместо того чтобы прибегать к Reflection.
Если вам нужно получить доступ к частным членам в модульных тестах, обычно это означает, что у соответствующего класса есть неподходящий интерфейс и/или пытается сделать слишком много. Поэтому либо его интерфейс должен быть пересмотрен, либо какой-то код должен быть извлечен в отдельный класс, где эти проблемные методы/полевые аксессоры могут быть преданы гласности.
Обратите внимание, что использование Reflection в целом приводит к тому, что код, который, будучи сложнее понять и поддерживать, также более хрупкий. Существует целый набор ошибок, которые в обычном случае будут обнаружены компилятором, но с Reflection они появляются только как исключения во время выполнения.
Обновление:, как отмечалось в @tackline, это касается только использования Reflection в пределах одного собственного тестового кода, а не внутренних систем тестирования. JUnit (и, вероятно, все другие подобные структуры) использует отражение для идентификации и вызова ваших методов тестирования - это оправданное и локализованное использование рефлексии. Было бы трудно или невозможно предоставить те же функции и удобство без использования Reflection. OTOH полностью инкапсулируется в рамках реализации фреймворка, поэтому он не усложняет и не компрометирует наш собственный тестовый код.
Ответ 2
Очень плохо изменить видимость производственного API только для тестирования. Эта видимость, вероятно, будет установлена на ее текущее значение по уважительным причинам и не будет изменена.
Использование отражения для модульного тестирования в основном прекрасное. Конечно, вы должны создавать свои классы для тестирования, чтобы требовалось меньшее отражение.
Spring например имеет ReflectionTestUtils
. Но его назначение задано mocks зависимостей, где spring должен был их вводить.
Тема глубже, чем "делать и не делать", и рассматривает, что должно быть проверено - необходимо ли проверять внутреннее состояние объектов или нет; следует ли нам допросить дизайн испытуемого класса; и др.
Ответ 3
С точки зрения TDD - Test Driven Design - это плохая практика. Я знаю, что вы не отмечали этот TDD, и не спрашивали об этом, но TDD - хорошая практика, и это идет вразрез с его зерном.
В TDD мы используем наши тесты для определения интерфейса класса - публичного интерфейса - и поэтому мы пишем тесты, которые взаимодействуют напрямую только с открытым интерфейсом. Мы обеспокоены этим интерфейсом; соответствующие уровни доступа являются важной частью дизайна, важной частью хорошего кода. Если вы обнаружите, что вам нужно проверить что-то частное, это обычно, по моему опыту, запах дизайна.
Ответ 4
Я рассматривал бы это как плохую практику, но просто изменение видимости в производственном коде не является хорошим решением, вам нужно посмотреть на причину. Либо у вас есть untestable API (то есть API не предоставляет достаточно для теста, чтобы получить дескриптор), поэтому вы хотите проверить личное состояние вместо этого, или ваши тесты слишком связаны с вашей реализацией, что сделает их только маргинальное использование при рефакторе.
Не зная больше о вашем случае, я не могу сказать больше, но действительно считается неправильной практикой использовать отражение. Лично я предпочел бы сделать тест статическим внутренним классом тестируемого класса, чем прибегать к рефлексии (если сказать, что непроверенная часть API не была под моим контролем), но в некоторых местах будет большая проблема с тем, что тестовый код находится в тот же пакет, что и производственный код, чем при использовании отражения.
РЕДАКТИРОВАТЬ: В ответ на ваше редактирование это, по крайней мере, такая же плохая практика, как использование Reflection, возможно, хуже. Способ, которым обычно обрабатывается, - использовать один и тот же пакет, но держать тесты в отдельной структуре каталогов. Если модульные тесты не принадлежат к тому же пакету, что и тестируемый класс, я не знаю, что делает.
Во всяком случае, вы все равно можете обойти эту проблему, используя защищенный (к сожалению, не закрытый для пакета, который действительно идеален для этого), тестируя подкласс следующим образом:
public class ClassUnderTest {
protect void methodExposedForTesting() {}
}
И внутри вашего unit test
class ClassUnderTestTestable extends ClassUnderTest {
@Override public void methodExposedForTesting() { super.methodExposedForTesting() }
}
И если у вас есть защищенный конструктор:
ClassUnderTest test = new ClassUnderTest(){};
Я не обязательно рекомендую вышесказанное для нормальных ситуаций, но ограничения, на которые вас просят работать, а не "лучшая практика" уже.
Ответ 5
Во-вторых, мысль о Божо:
Очень плохо изменить видимость производственного API только для тестирования.
Но если вы попытаетесь сделать простейшую вещь, которая могла бы работать, тогда было бы предпочтительнее писать Reflect API-шаблон, по крайней мере, пока ваш код по-прежнему новый/меняется. Я имею в виду, что брешь вручную изменять отраженные вызовы из вашего теста каждый раз, когда вы меняете имя метода или параметры, является ИМХО слишком большой нагрузкой и неправильным фокусом.
Упав в ловушку расслабляющей доступности только для тестирования, а затем непреднамеренно обращаясь к закрытому методу из другого производственного кода, я подумал о dp4j.jar: он вводит код API Reflection (Lombok-style), чтобы вы не меняли производственный код и не записывали Reflection API самостоятельно; dp4j заменяет ваш прямой доступ в unit test эквивалентным API отражения во время компиляции. Ниже приведен пример примера dp4j с JUnit.
Ответ 6
Я думаю, что ваш код должен быть протестирован двумя способами. Вы должны проверить свои общедоступные методы с помощью Unit Test, и это будет действовать как наше тестирование черного ящика. Поскольку ваш код разбит на управляемые функции (хороший дизайн), вы захотите Unit Test отдельные части с отражением, чтобы убедиться, что они работают независимо от процесса, единственный способ, которым я могу думать об этом, - это отражение поскольку они являются частными.
По крайней мере, это мое мышление в процессе Unit Test.
Ответ 7
Чтобы добавить к сказанному, рассмотрите следующее:
//in your JUnit code...
public void testSquare()
{
Class classToTest = SomeApplicationClass.class;
Method privateMethod = classToTest.getMethod("computeSquare", new Class[]{Integer.class});
String result = (String)privateMethod.invoke(new Integer(3));
assertEquals("9", result);
}
Здесь мы используем отражение для выполнения частного метода SomeApplicationClass.computeSquare(), проходящего в Integer и возвращающего результат String. Это приводит к тесту JUnit, который будет компилироваться отлично, но не выполняется во время выполнения, если возникает одно из следующих событий:
- Имя метода "computeSquare" переименовано
- Метод принимает другой тип параметра (например, изменение целого числа на длинный)
- Количество параметров изменяется (например, передается другим параметром)
- Возвращаемый тип метода изменяется (возможно, от String до Integer)
Вместо этого, если нет простого способа доказать, что computeSquare выполнил то, что он должен делать через открытый API, то ваш класс, вероятно, пытается многое сделать. Потяните этот метод в новый класс, который даст вам следующий тест:
public void testSquare()
{
String result = new NumberUtils().computeSquare(new Integer(3));
assertEquals("9", result);
}
Теперь (особенно если вы используете инструменты рефакторинга, доступные в современных IDE), изменение имени метода не влияет на ваш тест (так как ваша среда IDE также проведет рефакторинг теста JUnit) при изменении типа параметра, количество параметров или тип возвращаемого метода будет отмечать ошибку компиляции в вашем тестировании Junit, то есть вы не будете проверять тест JUnit, который компилируется, но не выполняется во время выполнения.
Моим последним моментом является то, что иногда, особенно при работе с устаревшим кодом, и вам нужно добавить новые функции, это может быть непросто сделать в отдельном проверяемом хорошо написанном классе. В этом случае моя рекомендация состояла бы в том, чтобы изолировать новые изменения кода с защищенными методами видимости, которые можно выполнить непосредственно в тестовом коде JUnit. Это позволяет вам создать базу тестового кода. В конце концов вы должны реорганизовать класс и извлечь свои дополнительные функции, но в то же время защищенная видимость вашего нового кода иногда может быть вашим лучшим способом в плане тестирования без значительного рефакторинга.