2010-05-11 5 views
80

В последние годы я всегда думал, что в Java Reflection широко используется во время модульного тестирования. Поскольку некоторые из переменных/методов, которые необходимо проверить, являются частными, необходимо как-то прочитать их значения. Я всегда думал, что API Reflection также используется для этой цели.Неплохо ли использовать рефлексию в модульном тестировании?

На прошлой неделе мне пришлось протестировать некоторые пакеты и, следовательно, написать некоторые тесты JUnit. Как всегда, я использовал Reflection для доступа к частным полям и методам. Но мой руководитель, который проверил код, был не очень доволен этим и сказал, что API Reflection не предназначен для использования для такого «взлома». Вместо этого он предложил изменить видимость в производственном кодексе.

Действительно ли испорчена практика отражения? Я не могу поверить, что-

Edit: я должен отметить, что я должен был, что все тесты находятся в отдельном пакете под названием тест (так, используя защищенный visibilty например, не было возможным решением тоже)

+0

Каким видом вы занимаетесь с отражением, множеством, попаданием или обоими? – fish

+0

@fish: я использую только для проверки того, установлены ли какие-то определенные значения. – RoflcoptrException

+7

Обычная практика в Java заключается в том, чтобы поместить тесты в один и тот же пакет, чтобы вы могли тестировать классы «закрытый» (по умолчанию). –

ответ

58

IMHO Reflection действительно должен быть только последним средством, зарезервированным для специального случая устаревшего кода модульного тестирования или API, который вы не можете изменить. Если вы тестируете свой собственный код, тот факт, что вам нужно использовать Reflection, означает, что ваш дизайн не тестируется, поэтому вы должны исправить это, вместо того чтобы прибегать к Reflection.

Если вам нужно получить доступ к закрытым членам в ваших модульных тестах, это обычно означает, что класс, о котором идет речь, имеет неподходящий интерфейс и/или пытается сделать слишком много. Поэтому либо его интерфейс должен быть пересмотрен, либо какой-то код должен быть извлечен в отдельный класс, где эти проблемные методы/полевые аксессоры могут быть преданы гласности.

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

Обновление: как отмечалось в @tackline, это касается только использования Reflection внутри собственного тестового кода, а не внутренних компонентов тестовой среды. JUnit (и, вероятно, все другие подобные структуры) использует отражение для идентификации и вызова ваших методов тестирования - это оправданное и локализованное использование рефлексии. Было бы трудно или невозможно предоставить те же функции и удобство без использования Reflection. OTOH полностью инкапсулируется в рамках реализации инфраструктуры, поэтому он не усложняет и не компрометирует наш собственный тестовый код.

+9

Отражение, очевидно, полезно в рамках тестирования для вызова тестов и издевательских интерфейсов в качестве альтернативы процессорам аннотаций времени компиляции. Но шляпа должна быть четко обозначена как отдельный вид использования. –

+1

@tackline, это то, что я действительно имел в виду, спасибо, что указали это. Я добавил разъяснение к моему ответу. –

+0

Вам не нужно много отборочных. Отражение должно в основном использоваться в качестве последнего средства, периода. Это делает невозможным рассуждение о программе. –

56

Это действительно плохо изменить видимость производственного API только для тестирования. Вероятность того, что видимость, вероятно, будет установлена ​​на текущее значение по уважительным причинам и не будет изменена.

Использование отражения для модульного тестирования в основном прекрасное. Конечно, вы должны design your classes for testability, так что требуется меньшее отражение.

Весна, например, имеет ReflectionTestUtils. Но его цель - установить издевательства зависимостей, где весна должна была их вводить.

Тема глубже, чем «делать & не», и считает, что должны быть проверены - нужно ли внутреннее состояние объектов, подлежащих испытанию или нет; следует ли нам допросить дизайн испытуемого класса; и т. д.

+1

вы бы объяснили свой нисходящий знак? – Bozho

+6

Есть две стороны изменения API производства. Изменение его только для тестов плохое, при обновлении вашего API/дизайна, чтобы ваш код был более подвержен проверке (и, следовательно, вы не меняете видимость только для тестов), является хорошей практикой. – deterb

+1

, если вы уверены, что то, что вы делаете, не имеет побочных эффектов - да. ;) – Bozho

7

Я рассматривал бы это как плохую практику, но просто изменение видимости в производственном коде не является хорошим решением, вам нужно посмотреть на причину.Либо у вас есть untestable API (то есть API не предоставляет достаточно для теста, чтобы получить дескриптор), поэтому вы хотите проверить личное состояние вместо этого, или ваши тесты слишком связаны с вашей реализацией, что сделает их только маргинальное использование при рефакторе.

Не зная больше о вашем случае, я не могу сказать больше, но это действительно считается плохой практикой использования reflection. Лично я предпочел бы сделать тест статическим внутренним классом тестируемого класса, чем прибегать к рефлексии (если сказать, что непроверенная часть API не была под моим контролем), но в некоторых местах будет большая проблема, когда тестовый код находится в тот же пакет, что и производственный код, чем с использованием отражения.

РЕДАКТИРОВАТЬ: В ответ на ваши изменения , что является, по крайней мере, такой же плохой практикой, как использование Reflection, возможно, хуже. Способ, которым обычно обрабатывается, - использовать один и тот же пакет, но держать тесты в отдельной структуре каталогов. Если модульные тесты не принадлежат к той же упаковке, что и тестируемый класс, я не знаю, что делает.

Во всяком случае, вы можете обойти эту проблему, используя защищенные (к сожалению, не пакет-частного, который действительно идеально подходит для этого) путем тестирования подкласса так:

public class ClassUnderTest { 
     protect void methodExposedForTesting() {} 
} 

И внутри модульного тестирования

class ClassUnderTestTestable extends ClassUnderTest { 
    @Override public void methodExposedForTesting() { super.methodExposedForTesting() } 
} 

И если у вас есть защищенный конструктор:

ClassUnderTest test = new ClassUnderTest(){}; 

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

+0

+1 Для изменения видимости рабочего кода не является хорошим решением –

3

Я думаю, что ваш код должен быть протестирован двумя способами. Вы должны протестировать свои общедоступные методы с помощью Unit Test, и это будет действовать как наше тестирование черного ящика. Поскольку ваш код разбит на управляемые функции (хороший дизайн), вы захотите, чтобы подразделение тестировало отдельные части с отражением, чтобы убедиться, что они работают независимо от процесса, единственный способ, которым я могу думать об этом, - это отражение с тех пор они частные.

По крайней мере, это мое мышление в модульном процессе тестирования.

28

С точки зрения TDD - Test Driven Дизайн - это плохая практика. Я знаю, что вы не отмечали этот TDD, и не спрашивали об этом, но TDD - хорошая практика, и это идет вразрез с его зерном.

В TDD мы используем наши тесты для определения интерфейса класса - public - и поэтому мы пишем тесты, которые взаимодействуют напрямую только с открытым интерфейсом. Мы обеспокоены этим интерфейсом; соответствующие уровни доступа являются важной частью дизайна, важной частью хорошего кода. Если вам нужно проверить что-то личное, то, как мне кажется, это запах дизайна.

+3

Согласовано. Относитесь к тестируемому объекту как к черному ящику - убедитесь, что выходы соответствуют входам. Если они не знают, что у вас есть проблемы с внутренними аспектами реализации. – AngerClown

+0

Но как насчет состояния объекта тестирования? –

+5

Внутреннее состояние объекта не имеет значения, если его внешнее поведение является правильным. –

3

Чтобы добавить к тому, что говорят другие, необходимо учитывать следующее:

//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» переименован
  • Метод принимает в другой тип параметра (например, изменение целого числа Long)
  • число параметров изменения (например, проходящее в другом параметре)
  • возвращаемого типа изменения методы (возможно, из строки в целое)

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

public void testSquare() 
{ 
    String result = new NumberUtils().computeSquare(new Integer(3)); 
    assertEquals("9", result); 
} 

Сейчас (особенно при использовании инструментов рефакторинга, доступные в современной среде IDE), изменение названия метода не оказывает никакого влияния на тесте (как ваша IDE также будет реорганизовать тест JUnit), изменяя тип параметра, количество параметров или тип возвращаемого метода будет отмечать ошибку компиляции в вашем тестировании Junit, то есть вы не будете проверять тест JUnit, который компилирует но не выполняется во время выполнения.

Моим последним моментом является то, что иногда, особенно при работе с устаревшим кодом, и вам нужно добавить новые функциональные возможности, это может быть непросто сделать в отдельном проверяемом хорошо написанном классе. В этом случае моя рекомендация состояла бы в том, чтобы изолировать новые изменения кода с защищенными методами видимости, которые можно выполнить непосредственно в тестовом коде JUnit. Это позволяет вам создать базу тестового кода. В конце концов вы должны реорганизовать класс и извлечь свои дополнительные функции, но в то же время защищенная видимость вашего нового кода может иногда быть вашим лучшим способом в плане тестирования без значительного рефакторинга.

5

Я вторая мысль о Bozho:

Это действительно плохо, чтобы изменить видимость производства API только для тестирования.

Но если вы попробуете do the simplest thing that could possibly work, тогда может быть предпочтительнее написать Reflect API-шаблон, по крайней мере, пока ваш код по-прежнему новый или меняется. Я имею в виду, что брешь вручную изменять отраженные вызовы из вашего теста каждый раз, когда вы меняете имя метода или параметры, является ИМХО слишком большой нагрузкой и неправильным фокусом.

Попав в ловушку релаксирующих доступа только для тестирования, а затем неумышленно доступ к был-частный метод из другого производственного кода я подумал о dp4j.jar: он вводит код Reflection API (Lombok-style), так что вы не изменить производственный код И не пишите API Reflection самостоятельно; dp4j заменяет ваш прямой доступ в модульном тесте эквивалентным API отражения во время компиляции. Вот example of dp4j with JUnit.

Смежные вопросы