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