Вопрос по mocking, mockito, java – Пересмешивание переменных-членов класса с использованием Mockito

110

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

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

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() класс вроде так. Я использую Mockito для этого.

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

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

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

Ваш Ответ

7   ответов
53

если вы не можете изменить свой код. Но мне нравится внедрение зависимостей, и 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());
   }
}

Это очень мило и просто.

Забавно, как начинающий, как я, начинающий тестировать, доверяю определенным библиотекам и фреймворкам. Я предполагал, что это просто плохая идея, указывающая на необходимость перепроектирования ... пока вы не показали мне этоявляется действительно возможно (очень четко и чисто) в Mockito. mike rodent
@IgorGanapolsky @ Ресурс - это аннотация, созданная / используемая средой Java Spring. Это способ указать Spring, что это bean / объект, управляемый Spring.stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Это не вещь мокито, но поскольку она используется в не тестируемом классе, она должна быть проверена в тесте. Grez.Kev
Что такое@Ресурс? IgorGanapolsky
Я думаю, что это лучший ответ, чем другие, потому что InjectMocks. sudocoder
72

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

Если ваш код не предоставляет способ сделать это, он неверно учтен для TDD (Test Driven Development).

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

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

@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 реализовано моделирование, где создается подкласс класса для моделирования; только экземпляры этого «подделанного» подкласса могут иметь поддельное поведение, поэтому вам нужно, чтобы протестированный код использовал их вместо любого другого экземпляра.

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

тогда вы можете использовать powerMockit и вызвать

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

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

1

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

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

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

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

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

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

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class));
31

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());
}

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

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

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

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

Похожие вопросы