Пересмешивание переменных-членов класса с использованием Mockito - PullRequest
119 голосов
/ 25 января 2012

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

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

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

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

Ответы [ 8 ]

76 голосов
/ 25 января 2012

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

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

54 голосов
/ 04 июля 2013

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

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

32 голосов
/ 25 января 2012

Если вы внимательно посмотрите на свой код, вы увидите, что свойство 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());
}

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

6 голосов
/ 19 марта 2015

У меня была та же проблема, когда частное значение не было установлено, потому что 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
}

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

6 голосов
/ 31 марта 2014

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

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

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

1 голос
/ 22 апреля 2014

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

Если вы не можете изменить код, чтобы сделать его более тестируемым, 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));
0 голосов
/ 19 апреля 2019

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

Whitebox.setInternalState(first, "second", sec);
0 голосов
/ 04 июля 2013

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

...