Как мне проверить приватную функцию или класс, который имеет закрытые методы, поля или внутренние классы?

Как мне выполнить модульное тестирование (используя xUnit) класс, который имеет внутренние закрытые методы, поля или вложенные классы? Или функция, которая делается частной, имея внутреннюю связь (static в C/C++) или в частном ( анонимном) пространстве имен?

Кажется плохим изменить модификатор доступа для метода или функции просто для того, чтобы можно было запустить тест.

60 ответов

Решение

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

Внутренне мы используем помощников, чтобы получить / установить private а также private static переменные, а также вызывать private а также private static методы. Следующие шаблоны позволят вам делать практически все, что связано с закрытыми методами и полями. Конечно, вы не можете изменить private static final переменные через отражение.

Method method = TargetClass.getDeclaredMethod(methodName, argClasses);
method.setAccessible(true);
return method.invoke(targetObject, argObjects);

И для полей:

Field field = TargetClass.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);

Заметки:
1. TargetClass.getDeclaredMethod(methodName, argClasses) позволяет вам посмотреть в private методы. То же самое относится и к getDeclaredField,
2. The setAccessible(true) требуется поиграть с рядовыми.

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

  1. Закрытый метод - мертвый код
  2. Рядом с классом, который вы тестируете, чувствуется запах дизайна
  3. Метод, который вы пытаетесь проверить, не должен быть закрытым

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

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

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

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

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

Строго говоря, вы не должны писать модульные тесты, которые напрямую тестируют частные методы. То, что вы должны тестировать, это публичный контракт, который класс имеет с другими объектами; Вы никогда не должны напрямую проверять внутренности объекта. Если другой разработчик хочет внести небольшое внутреннее изменение в класс, которое не влияет на публичный контракт классов, он / она должен изменить ваш тест на основе отражения, чтобы убедиться, что он работает. Если вы будете делать это несколько раз в течение всего проекта, модульные тесты перестанут быть полезным измерением работоспособности кода и станут помехой для разработки и раздражением для команды разработчиков.

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

Из этой статьи: Тестирование частных методов с помощью JUnit и SuiteRunner (Билл Веннерс), у вас в основном есть 4 варианта:

  • Не проверяйте частные методы.
  • Предоставьте доступ к пакету методов.
  • Используйте вложенный тестовый класс.
  • Используйте отражение.

Обычно модульный тест предназначен для использования открытого интерфейса класса или модуля. Следовательно, частные методы - это детали реализации, которые вы не ожидаете явно протестировать.

Всего два примера, где я хотел бы протестировать приватный метод:

  1. Процедуры дешифрования - я бы не хотел, чтобы их кто-то видел, чтобы их можно было увидеть только ради тестирования, иначе любой может использовать их для дешифрования. Но они присущи коду, сложны и должны работать всегда (очевидное исключение - отражение, которое может использоваться для просмотра даже частных методов в большинстве случаев, когда SecurityManager не настроен, чтобы предотвратить это).
  2. Создание SDK для общественного потребления. Здесь публика приобретает совершенно иное значение, поскольку это код, который может видеть весь мир (не только внутренний для моего приложения). Я помещаю код в приватные методы, если не хочу, чтобы пользователи SDK видели его - я не вижу в этом запаха кода, а просто как работает программирование SDK. Но, конечно же, мне все еще нужно протестировать свои частные методы, и именно там они действительно функционируют.

Я понимаю идею только тестирования "контракта". Но я не вижу, чтобы кто-то мог выступать за то, чтобы не тестировать код - ваш пробег может отличаться

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

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

В Spring Framework вы можете протестировать закрытые методы, используя этот метод:

ReflectionTestUtils.invokeMethod()

Например:

ReflectionTestUtils.invokeMethod(TestClazz, "createTest", "input data");

Другой подход, который я использовал, - это изменить приватный метод для упаковки приватного или защищенного, а затем дополнить его аннотацией @VisibleForTesting библиотеки Google Guava.

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

Например, если тестируемый метод находится в src/main/java/mypackage/MyClass.java тогда ваш тестовый звонок должен быть помещен в src/test/java/mypackage/MyClassTest.java, Таким образом, вы получили доступ к тестовому методу в вашем тестовом классе.

Чтобы протестировать унаследованный код с большими и причудливыми классами, часто очень полезно иметь возможность протестировать один приватный (или публичный) метод, который я пишу прямо сейчас.

Я использую пакет junitx.util.PrivateAccessor для Java . Много полезных однострочников для доступа к приватным методам и приватным полям.

import junitx.util.PrivateAccessor;

PrivateAccessor.setField(myObjectReference, "myCrucialButHardToReachPrivateField", myNewValue);
PrivateAccessor.invoke(myObjectReference, "privateMethodName", java.lang.Class[] parameterTypes, java.lang.Object[] args);

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

Возможно, есть смысл в тестировании частных методов класса, особенно при разработке на основе тестов, когда вы хотите разработать небольшие тесты, прежде чем писать какой-либо код.

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

Преимущества:

  • Может тестировать до мелкой гранулярности

Недостатки:

  • Тестовый код должен находиться в том же файле, что и исходный код, который может быть сложнее поддерживать
  • Аналогично с выходными файлами.class они должны оставаться в том же пакете, который объявлен в исходном коде.

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

Вот замысловатый пример того, как это будет работать:

// Import statements and package declarations

public class ClassToTest
{
    private int decrement(int toDecrement) {
        toDecrement--;
        return toDecrement;
    }

    // Constructor and the rest of the class

    public static class StaticInnerTest extends TestCase
    {
        public StaticInnerTest(){
            super();
        }

        public void testDecrement(){
            int number = 10;
            ClassToTest toTest= new ClassToTest();
            int decremented = toTest.decrement(number);
            assertEquals(9, decremented);
        }

        public static void main(String[] args) {
            junit.textui.TestRunner.run(StaticInnerTest.class);
        }
    }
}

Внутренний класс будет скомпилирован в ClassToTest$StaticInnerTest,

См. Также: Java Совет 106: статические внутренние классы для удовольствия и выгоды

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

  1. Все методы должны быть небольшими и целенаправленными (легко тестировать, легко находить, что не так)
  2. Используйте инструменты покрытия кода. Мне нравится Cobertura (о, счастливый день, похоже, вышла новая версия!)

Запустите покрытие кода на модульных тестах. Если вы видите, что методы не полностью протестированы, добавьте тесты, чтобы увеличить охват. Стремитесь к 100% охвату кода, но понимайте, что вы, вероятно, не получите его.

При использовании Spring ReflectionTestUtils предоставляет несколько удобных инструментов, которые помогут здесь с минимальными усилиями. Например, чтобы настроить макет на приватном члене без необходимости добавлять нежелательный общедоступный установщик:

ReflectionTestUtils.setField(theClass, "theUnsettableField", theMockObject);

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

Тестирование частных методов должно быть проверено путем отладки перед запуском ваших модульных тестов с открытыми методами.

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

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

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

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

Если вы хотите проверить сложные условия ошибки, возникающие из вспомогательного метода, вы можете пойти дальше. Извлеките интерфейс из вспомогательного класса, добавьте открытый метод получения и установки в исходный класс, чтобы внедрить вспомогательный класс (используемый через его интерфейс), а затем внедрите фиктивную версию вспомогательного класса в исходный класс, чтобы проверить, как исходный класс реагирует на исключения из помощника. Этот подход также полезен, если вы хотите протестировать исходный класс без тестирования вспомогательного класса.

Тестирование приватных методов нарушает инкапсуляцию вашего класса, потому что каждый раз, когда вы изменяете внутреннюю реализацию, вы нарушаете клиентский код (в данном случае, тесты).

Так что не проверяйте частные методы.

Ответ со страницы часто задаваемых вопросов JUnit.org:

Но если ты должен...

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

Если вы используете JDK 1.6 или выше и аннотируете свои тесты с помощью @Test, вы можете использовать Dp4j для добавления отражения в ваши методы тестирования. Подробнее о том, как его использовать, смотрите в этом тестовом скрипте.

PS Я основной участник Dp4j, спросите simpatico, если вам нужна помощь.:)

Если вы хотите протестировать приватные методы унаследованного приложения, в котором вы не можете изменить код, одним из вариантов для Java является jMockit, который позволит вам создавать макеты для объекта, даже когда они являются закрытыми для класса.

PowerMockito создан для этого. Используйте maven зависимость

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-mockito-release-full</artifactId>
    <version>1.6.4</version>
</dependency>

Тогда вы можете сделать

import org.powermock.reflect.Whitebox;
...
MyClass sut = new MyClass();
SomeType rval = Whitebox.invokeMethod(sut, "myPrivateMethod", params, moreParams);

Если вы используете JUnit, взгляните на junit-addons. Он может игнорировать модель безопасности Java и получать доступ к закрытым методам и атрибутам.

Я склонен не проверять частные методы. Там лежит безумие. Лично я считаю, что вам следует проверять только открытые интерфейсы (включая защищенные и внутренние методы).

Я бы посоветовал вам немного реорганизовать свой код. Когда вам нужно начать думать об использовании рефлексии или чего-то другого для простого тестирования кода, что-то не так с вашим кодом.

Вы упомянули разные типы проблем. Давайте начнем с частных полей. В случае приватных полей я бы добавил новый конструктор и вставил в него поля. Вместо этого:

public class ClassToTest {

    private final String first = "first";
    private final List<String> second = new ArrayList<>();
    ...
}

Я бы использовал это:

public class ClassToTest {

    private final String first;
    private final List<String> second;

    public ClassToTest() {
        this("first", new ArrayList<>());
    }

    public ClassToTest(final String first, final List<String> second) {
        this.first = first;
        this.second = second;
    }
    ...
}

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

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

public ClassToTest() {
    this(...);
}

public ClassToTest(final Callable<T> privateMethodLogic) {
    this.privateMethodLogic = privateMethodLogic;
}

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

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

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

Вот моя общая функция для проверки приватных полей:

protected <F> F getPrivateField(String fieldName, Object obj)
    throws NoSuchFieldException, IllegalAccessException {
    Field field =
        obj.getClass().getDeclaredField(fieldName);

    field.setAccessible(true);
    return (F)field.get(obj);
}

Пожалуйста, смотрите ниже для примера;

Необходимо добавить следующую инструкцию импорта:

import org.powermock.reflect.Whitebox;

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

Whitebox.invokeMethod(obj, "privateMethod", "param1");

Закрытый метод доступен только в пределах одного класса. Таким образом, нет способа протестировать "закрытый" метод целевого класса из какого-либо тестового класса. Выход в том, что вы можете выполнить модульное тестирование вручную или изменить свой метод с "частного" на "защищенный".

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

Если все вышеперечисленное не соответствует вашим требованиям, используйте способ отражения для доступа к приватному методу.

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

Если вы сделаете это, неплохо будет использовать инструмент покрытия кода (например, Эмму), чтобы увидеть, выполняются ли ваши частные методы из ваших тестов.

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

Если у вас есть код с закрытыми методами, полями или конструкторами, вы можете использовать BoundBox. Это именно то, что вы ищете. Ниже приведен пример теста, который обращается к двум закрытым полям действия Android для его проверки:

@UiThreadTest
public void testCompute() {

    // Given
    boundBoxOfMainActivity = new BoundBoxOfMainActivity(getActivity());

    // When
    boundBoxOfMainActivity.boundBox_getButtonMain().performClick();

    // Then
    assertEquals("42", boundBoxOfMainActivity.boundBox_getTextViewMain().getText());
}

BoundBox позволяет легко тестировать приватные / защищенные поля, методы и конструкторы. Вы даже можете получить доступ к вещам, которые скрыты по наследству. Действительно, BoundBox нарушает инкапсуляцию. Это даст вам доступ ко всему этому через рефлексию, НО все проверяется во время компиляции.

Он идеально подходит для тестирования некоторого устаревшего кода. Используйте это осторожно.;)

https://github.com/stephanenicolas/boundbox

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

Если они настолько велики, достаточно велики, что каждый из этих закрытых членов по сложности является "единичным", рассмотрите возможность рефакторинга таких закрытых членов из этого класса.

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

Я хочу поделиться своим правилом о тестировании, которое особенно относится к этой теме:

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

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

Зачем нам вообще нужны частные поля / методы / классы, если мы хотим вести разработку через тестирование? Должны ли мы тогда объявить все как частный пакет или даже как общедоступный, чтобы мы могли протестировать без каких-либо усилий? - Я так не думаю.

С другой точки зрения. Тесты не должны ухудшать производительность и выполнение производственного приложения. Если вы измените производственный код только для облегчения тестирования, вы можете каким-то образом снизить производительность и выполнение приложения. И если вы начнете изменять частный доступ к пакету private, у вас могут в конечном итоге возникнуть «гениальные идеи» о добавлении еще некоторого кода в исходный класс, что создаст дополнительный шум для удобочитаемости и может снизить производительность приложения.

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

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

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