Пересмешивание переменных-членов класса с использованием Mockito

136

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

Предположим, у меня есть два таких класса -

public class First {

    Second second ;

    public First(){
        second = new Second();
    }

    public String doSecond(){
        return second.doSecond();
    }
}

class Second {

    public String doSecond(){
        return "Do Something";
    }
}

Скажем, я пишу модульный тест для First.doSecond()метода тестирования . Однако, предположим, я хочу, чтобы Second.doSecond()класс Mock был таким. Для этого я использую Mockito.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Я вижу, что насмешка не действует, а утверждение не выполняется. Нет возможности издеваться над переменными-членами класса, который я хочу протестировать. ?

Ананд Хеммиге
источник

Ответы:

86

Вам необходимо предоставить способ доступа к переменным-членам, чтобы вы могли передать имитацию (наиболее распространенными способами были бы метод установки или конструктор, который принимает параметр).

Если в вашем коде нет возможности сделать это, значит, он неверно учтен для TDD (разработка через тестирование).

kittylyst
источник
4
Спасибо. Вижу. Мне просто интересно, как я могу затем выполнять интеграционные тесты, используя макет, где может быть много внутренних методов, классов, которые, возможно, нужно будет имитировать, но не обязательно доступны для установки с помощью setXXX () заранее.
Anand Hemmige
2
Используйте структуру внедрения зависимостей с тестовой конфигурацией. Составьте диаграмму последовательности интеграционного теста, который вы пытаетесь выполнить. Разложите диаграмму последовательности на объекты, которыми вы действительно можете управлять. Это означает, что если вы работаете с классом фреймворка, у которого есть показанный выше антишаблон зависимого объекта, то вы должны рассматривать объект и его плохо факторизованный член как единое целое с точки зрения диаграммы последовательности. Будьте готовы скорректировать факторинг любого контролируемого вами кода, чтобы сделать его более тестируемым.
kittylyst
9
Уважаемый @kittylyst, да, наверное, это неправильно с точки зрения TDD или с какой-либо рациональной точки зрения. Но иногда разработчик работает там, где вообще ничего не имеет смысла, и единственная цель, которая у него есть, - просто завершить истории, которые вы назначили, и уйти. Да, это неправильно, в этом нет смысла, ключевые решения принимают неквалифицированные люди и все такое. Так что, в конце концов, антипаттерны очень много выигрывают.
amanas
1
Мне любопытно, если член класса не имеет причин для установки извне, почему мы должны создавать сеттер только с целью его тестирования? Представьте, что второй класс - это на самом деле менеджер или инструмент файловой системы, инициализированный во время создания объекта для тестирования. У меня есть все причины поиздеваться над этим менеджером файловой системы, чтобы протестировать первый класс, и нет причин сделать его доступным. Я могу сделать это на Python, так почему бы не с Mockito?
Зангдар,
65

Это невозможно, если вы не можете изменить свой код. Но мне нравится внедрение зависимостей, и Mockito его поддерживает:

public class First {    
    @Resource
    Second second;

    public First() {
        second = new Second();
    }

    public String doSecond() {
        return second.doSecond();
    }
}

Ваш тест:

@RunWith(MockitoJUnitRunner.class)
public class YourTest {
   @Mock
   Second second;

   @InjectMocks
   First first = new First();

   public void testFirst(){
      when(second.doSecond()).thenReturn("Stubbed Second");
      assertEquals("Stubbed Second", first.doSecond());
   }
}

Это очень красиво и просто.

Janning
источник
2
Я думаю, что это лучший ответ, чем другие, потому что InjectMocks.
Sudocoder
Забавно, как новичок в тестировании вроде меня начинает доверять определенным библиотекам и фреймворкам. Я предполагал, что это просто Плохая идея, указывающая на необходимость редизайна ... пока вы не показали мне, что это действительно возможно (очень четко и чисто) в Mockito.
Майк грызун
9
Что такое @Resource ?
Игорь Ганапольский
3
@IgorGanapolsky @ Ресурс - это аннотация, созданная / используемая фреймворком Java Spring. Это способ указать Spring, что это bean-объект / объект, управляемый Spring. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Это не mockito, но поскольку он используется в не тестирующем классе, его нужно высмеивать в тест.
Grez.Kev 04
Я не понимаю этот ответ. Вы говорите, что это невозможно, а потом показываете, что это возможно? Что именно здесь невозможно?
Goldname
35

Если вы внимательно посмотрите на свой код, вы увидите, что secondсвойство в вашем тесте по-прежнему является экземпляром Second, а не макетом (вы не передаете макет firstв свой код).

Самый простой способ будет создать сеттер для secondв Firstклассе и передать ему издеваться явно.

Как это:

public class First {

Second second ;

public First(){
    second = new Second();
}

public String doSecond(){
    return second.doSecond();
}

    public void setSecond(Second second) {
    this.second = second;
    }


}

class Second {

public String doSecond(){
    return "Do Something";
}
}

....

public void testFirst(){
Second sec = mock(Second.class);
when(sec.doSecond()).thenReturn("Stubbed Second");


First first = new First();
first.setSecond(sec)
assertEquals("Stubbed Second", first.doSecond());
}

Другой вариант - передать Secondэкземпляр как Firstпараметр конструктора.

Если вы не можете изменить код, я думаю, единственный вариант - использовать отражение:

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");


    First first = new First();
    Field privateField = PrivateObject.class.
        getDeclaredField("second");

    privateField.setAccessible(true);

    privateField.set(first, sec);

    assertEquals("Stubbed Second", first.doSecond());
}

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

soulcheck
источник
Понял. Я, вероятно, приму ваше первое предложение.
Anand Hemmige
Просто любопытно, есть ли способ или API, о котором вы знаете, который может имитировать объект / метод на уровне приложения или уровня пакета. ? Я предполагаю, что я говорю о том, что в приведенном выше примере, когда я издеваюсь над объектом «Второй», есть ли способ переопределить каждый экземпляр Second, который используется на протяжении жизненного цикла тестирования. ?
Anand Hemmige
@AnandHemmige на самом деле второй (конструктор) чище, так как он позволяет избежать создания ненужных экземпляров `Second '. Таким образом, ваши классы будут хорошо разделены.
soulcheck
10
Mockito предоставляет несколько хороших аннотаций, позволяющих вам вставлять свои макеты в частные переменные. Добавьте аннотацию Second с помощью @Mockи аннотируйте First с помощью @InjectMocksи создайте экземпляр First в инициализаторе. Mockito автоматически сделает все возможное, чтобы найти место для внедрения второго макета в первый экземпляр, включая установку частных полей, соответствующих типу.
jhericks
@Mockбыло примерно в 1.5 (может и раньше, не уверен). Введены 1.8.3, @InjectMocksа также @Spyи @Captor.
jhericks
7

Если вы не можете изменить переменную-член, то другой способ - использовать powerMockit и вызвать

Second second = mock(Second.class)
when(second.doSecond()).thenReturn("Stubbed Second");
whenNew(Second.class).withAnyArguments.thenReturn(second);

Теперь проблема в том, что ЛЮБОЙ вызов new Second вернет тот же фиктивный экземпляр. Но в вашем простом случае это сработает.

user1509463
источник
6

У меня была такая же проблема, когда частное значение не было установлено, потому что Mockito не вызывает суперконструкторы. Вот как я дополняю насмешку отражением.

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

public class TestUtils {
    // get a static class value
    public static Object reflectValue(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(classToReflect, fieldNameValueToFetch);
            reflectField.setAccessible(true);
            Object reflectValue = reflectField.get(classToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // get an instance value
    public static Object reflectValue(Object objToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameValueToFetch);
            Object reflectValue = reflectField.get(objToReflect);
            return reflectValue;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch);
        }
        return null;
    }
    // find a field in the class tree
    public static Field reflectField(Class<?> classToReflect, String fieldNameValueToFetch) {
        try {
            Field reflectField = null;
            Class<?> classForReflect = classToReflect;
            do {
                try {
                    reflectField = classForReflect.getDeclaredField(fieldNameValueToFetch);
                } catch (NoSuchFieldException e) {
                    classForReflect = classForReflect.getSuperclass();
                }
            } while (reflectField==null || classForReflect==null);
            reflectField.setAccessible(true);
            return reflectField;
        } catch (Exception e) {
            fail("Failed to reflect "+fieldNameValueToFetch +" from "+ classToReflect);
        }
        return null;
    }
    // set a value with no setter
    public static void refectSetValue(Object objToReflect, String fieldNameToSet, Object valueToSet) {
        try {
            Field reflectField  = reflectField(objToReflect.getClass(), fieldNameToSet);
            reflectField.set(objToReflect, valueToSet);
        } catch (Exception e) {
            fail("Failed to reflectively set "+ fieldNameToSet +"="+ valueToSet);
        }
    }

}

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

@Test
public void testWithRectiveMock() throws Exception {
    // mock the base class using Mockito
    ClassToMock mock = Mockito.mock(ClassToMock.class);
    TestUtils.refectSetValue(mock, "privateVariable", "newValue");
    // and this does not prevent normal mocking
    Mockito.when(mock.somthingElse()).thenReturn("anotherThing");
    // ... then do your asserts
}

Я изменил свой код из моего реального проекта здесь, на странице. Может быть проблема с компиляцией или две. Думаю, вы поняли общую идею. Не стесняйтесь брать код и использовать его, если он вам пригодится.

Дейв
источник
Не могли бы вы объяснить свой код реальным вариантом использования? как публичный класс tobeMocker () {частный ClassObject classObject; } Где classObject равно заменяемому объекту.
Джаспер Ланкхорст,
В вашем примере, если ToBeMocker instance = new ToBeMocker (); и ClassObject someNewInstance = new ClassObject () {@Override // что-то вроде внешней зависимости}; затем TestUtils.refelctSetValue (экземпляр, «classObject», someNewInstance); Обратите внимание, что вам нужно выяснить, что вы хотите переопределить для издевательства. Допустим, у вас есть база данных, и это переопределение вернет значение, поэтому вам не нужно выбирать. Совсем недавно у меня была служебная шина, и я не хотел обрабатывать сообщение, но хотел убедиться, что он его получил. Таким образом, я установил экземпляр частной шины следующим образом - Полезно?
Дэйв
Вы должны представить себе, что в этом комментарии было форматирование. Он был удален. Кроме того, это не будет работать с Java 9, так как это заблокирует частный доступ. Нам придется работать с некоторыми другими конструкциями, когда у нас будет официальный релиз и мы сможем работать в его фактических пределах.
Дэйв
1

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

Если вы не можете изменить код, чтобы сделать его более тестируемым, PowerMock: https://code.google.com/p/powermock/

PowerMock расширяет Mockito (поэтому вам не нужно изучать новую фиктивную структуру), предоставляя дополнительные функции. Это включает возможность иметь конструктор, возвращающий макет. Мощный, но немного сложный - поэтому используйте его с умом.

Вы используете другой Mock-раннер. И вам нужно подготовить класс, который будет вызывать конструктор. (Обратите внимание, что это обычная проблема - подготовьте класс, который вызывает конструктор, а не созданный класс)

@RunWith(PowerMockRunner.class)
@PrepareForTest({First.class})

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

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
jwepurchase
источник
0

Да, это можно сделать, как показывает следующий тест (написанный с помощью API-интерфейса JMockit, который я разрабатываю):

@Test
public void testFirst(@Mocked final Second sec) {
    new NonStrictExpectations() {{ sec.doSecond(); result = "Stubbed Second"; }};

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

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

Рожерио
источник
3
вопрос был не в том, лучше ли JMockit или Mockito, а в том, как это сделать в Mockito. Придерживайтесь создания лучшего продукта, а не ищите возможности уничтожить конкурентов!
TheZuck
8
На оригинальном плакате только сказано, что он использует Mockito; это только подразумевается, что Mockito является фиксированным и жестким требованием, поэтому намек на то, что JMockit может справиться с этой ситуацией, не так уж неуместен.
Bombe
0

Если вам нужна альтернатива ReflectionTestUtils из Spring в mockito, используйте

Whitebox.setInternalState(first, "second", sec);
Шимон Зволиньски
источник
Добро пожаловать в стек переполнения! Есть и другие ответы, которые задают вопрос OP, и они были опубликованы много лет назад. Публикуя ответ, убедитесь, что вы добавили либо новое решение, либо существенно лучшее объяснение, особенно когда отвечаете на старые вопросы или комментируете другие ответы.
help-info.de