Смеющиеся переменные-члены класса с использованием 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";
    }
}

Скажем, я пишу unit test для тестирования метода 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());
}

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

Ответ 1

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

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

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

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

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

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

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

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

Ответ 7

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

Ответ 8

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

Whitebox.setInternalState(first, "second", sec);