2012-01-24 4 views
91

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

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

ответ

58

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

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

+1

Спасибо. Вижу. Мне просто интересно, как я могу выполнить интеграционные тесты, используя mock, где может быть много внутренних методов, классов, которые, возможно, нужно издеваться, но не обязательно доступны для установки через setXXX() перед началом работы. –

+2

Используйте инфраструктуру внедрения зависимостей с тестовой конфигурацией. Составьте диаграмму последовательности теста интеграции, который вы пытаетесь сделать. Фактор диаграммы последовательности в объекты, которые вы можете контролировать. Это означает, что если вы работаете с классом фреймворка, у которого есть анти-шаблон зависимого объекта, который вы показываете выше, то вы должны рассматривать объект и его слабозависимый элемент как единое целое в терминах диаграммы последовательности. Будьте готовы настроить факторинг любого кода, который вы контролируете, чтобы сделать его более проверяемым. – kittylyst

+4

Уважаемый @kittylyst, да, возможно, это неправильно с точки зрения TDD или с любой рациональной точки зрения. Но иногда разработчик работает в тех местах, где ничего не имеет смысла, и единственная цель, которую у вас есть, - это просто завершить рассказы, которые вы назначили, и уйти. Да, это неправильно, это бессмысленно, неквалифицированные люди принимают ключевые решения и все такое. Таким образом, в конце концов, анти-шаблоны выигрывают много. – amanas

30

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

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

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

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

+0

Got it. Я, вероятно, поеду с вашим первым предложением. –

+0

Любопытно, есть ли какой-либо способ или API, о которых вы знаете, что может издеваться над объектом/методом на уровне приложения или уровне пакета. ? Я предполагаю, что, как я сказал, в приведенном выше примере, когда я издеваюсь над «вторым» объектом, есть способ, которым он может переопределить каждый экземпляр Second, который используется через жизненный цикл тестирования. ? –

+0

@AnandHemmige фактически второй (конструктор) является более чистым, так как он избегает создания ненужных «вторых» экземпляров. Таким образом, ваши классы прекрасно отделяются. – soulcheck

42

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

Это очень приятно и легко.

+0

Я думаю, что это лучший ответ, чем другие, потому что InjectMocks. – sudocoder

+0

Забавно, как можно, как новичок, как я, доверять определенным библиотекам и фреймворкам. Я предполагал, что это была просто плохая идея, указывающая на необходимость перепроектирования ... пока вы не показали мне это ** ** действительно возможно (очень ясно и чисто) в Мокито. –

+3

Что такое ** @ Resource **? –

-1

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

+3

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

+8

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

6

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

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

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

1

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

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

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

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

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

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

whenNew(Second.class).withAnyArguments().thenReturn(mock(Second.class)); 
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 
} 

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

+0

Не могли бы вы объяснить свой код фактической usecase? например Открытый класс tobeMocker() { private ClassObject classObject; } Где classObject равно подлежащему замене объекту. –

+0

В вашем примере, если ToBeMocker instance = new ToBeMocker(); и ClassObject someNewInstance = new ClassObject() { @Override // что-то вроде внешней зависимости }; then TestUtils.refelctSetValue (экземпляр, "classObject", someNewInstance); Обратите внимание, что вам нужно выяснить, что вы хотите переопределить для насмешек. Допустим, у вас есть база данных, и это переопределение вернет значение, поэтому вам не нужно выбирать. Совсем недавно у меня была служебная шина, в которой я не хотел обрабатывать сообщение, но хотел, чтобы он его получил. Таким образом, я установил экземпляр частного автобуса таким образом - полезный? – dave

+0

Вам нужно будет представить, что в этом комментарии было форматирование. Он был удален. Кроме того, это не будет работать с Java 9, так как это заблокирует частный доступ. Нам придется работать с некоторыми другими конструкциями, как только у нас будет официальный выпуск, и мы можем работать с его фактическими границами. – dave

Смежные вопросы