it-roy-ru.com

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

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

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

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(). Тем не менее, предположим, я хочу Mock 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());
}

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

116
Anand Hemmige

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

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

76
kittylyst

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

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

53
Janning

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

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

31
soulcheck

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

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

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

6
user1509463

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

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

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

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

Whitebox.setInternalState(first, "second", sec);
0
Szymon Zwoliński

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

0
Rogério