Знущання зі змінними членів класу за допомогою Mockito


136

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

Припустимо, у мене є два такі класи -

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()клас Mock виглядав так. Для цього я використовую Mockito.

public void testFirst(){
    Second sec = mock(Second.class);
    when(sec.doSecond()).thenReturn("Stubbed Second");

    First first = new First();
    assertEquals("Stubbed Second", first.doSecond());
}

Я бачу, що глузування не спрацьовує, і твердження провалюється. Чи немає способу знущатися зі змінних членів класу, який я хочу перевірити. ?

Відповіді:


86

Вам потрібно надати спосіб доступу до змінних-членів, щоб ви могли передати макет (найпоширенішими способами буде метод сеттера або конструктор, який приймає параметр).

Якщо ваш код не передбачає способу цього, він неправильно враховується для TDD (Test Driven Development).


4
Дякую. Я бачу це. Мені просто цікаво, як я можу потім виконувати тести інтеграції, використовуючи макет, де може бути багато внутрішніх методів, класів, які, можливо, потрібно знущатися, але не обов'язково доступні для встановлення через setXXX () перед рукою.
Ананд Хемміге

2
Використовуйте рамку введення залежності, з тестовою конфігурацією. Складіть схему послідовності тесту інтеграції, який ви намагаєтеся зробити. Розділіть діаграму послідовностей на об'єкти, якими ви можете фактично керувати. Це означає, що якщо ви працюєте з класним фреймворком, який має залежний антидіапазон об'єкта, який ви показуєте вище, то ви повинні розглядати об'єкт та його погано факторний член як єдину одиницю з точки зору діаграми послідовностей. Будьте готові коригувати факторинг будь-якого коду, яким ви керуєте, щоб зробити його більш зрозумілим.
kittylyst

9
Шановний @kittylyst, так, напевно, це неправильно з точки зору TDD або з будь-якого раціонального погляду. Але іноді розробник працює в місцях, де взагалі нічого не має сенсу, і єдина мета, яка у вас є, - це лише виконати призначені вами історії та відійти. Так, це неправильно, немає сенсу, некваліфіковані люди приймають ключові рішення та все таке. Отже, наприкінці дня анти-шаблони виграють багато.
amanas

1
Мені цікаво з цього приводу, якщо у члена класу немає причин встановлювати його з зовнішності, чому ми повинні створити сеттер саме для того, щоб перевірити його? Уявіть, що "Другий" клас - це насправді менеджер або інструмент FileSystem, ініціалізований під час побудови об'єкта для тестування. У мене є всі причини, щоб хотіти знущатися над цим менеджером FileSystem, щоб перевірити перший клас і нульову причину, щоб зробити його доступним. Я можу це зробити в Python, так чому б не з Mockito?
Зангдар

65

Це неможливо, якщо ви не можете змінити свій код. Але мені подобається введення залежності, і 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());
   }
}

Це дуже приємно і просто.


2
Я думаю, що це краща відповідь, ніж інші, тому що InjectMocks.
Судокодер

Смішно, як людині, як тестувальній новичці, як я, довіряти певні бібліотеки та рамки. Я припускав, що це просто погана ідея, що вказує на необхідність переробити дизайн ... поки ви не показали мені, що це справді можливо (дуже чітко і чисто) в Mockito.
мійський гризун

9
Що таке @Resource ?
ІгорГанапольський

3
@IgorGanapolsky the @ Resource - це анотація, створена / використана рамкою Java Spring. Це спосіб вказати до Весни, що це боб / об'єкт, яким управляє Весна. stackoverflow.com/questions/4093504/resource-vs-autowired baeldung.com/spring-annotations-resource-inject-autowire Це не Mockito річ, а тому , що вона використовується в не клас тестування він повинен бути знущалися в тест.
Грец.Кев

Я не розумію цієї відповіді. Ви кажете, що це неможливо, тоді ви показуєте, що це можливо? Що саме тут неможливо?
Goldname

35

Якщо ви уважно подивитесь на свій код, то побачите, що 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());
}

Але ви, мабуть, можете, оскільки це рідко робити тести на код, який ви не контролюєте (хоча можна уявити сценарій, коли вам доведеться перевірити зовнішню бібліотеку, оскільки її автор не зробив :)


Зрозумів. Я, мабуть, піду з вашою першою пропозицією.
Ананд Хемміге

Цікаво, чи є вам відомий спосіб чи API, який може знущатися над об'єктом / методом на рівні програми або на рівні пакета. ? Я здогадуюсь, про що я говорю, - у наведеному вище прикладі, коли я знущаюся над об'єктом "Другий", чи є спосіб, що він може замінити кожен екземпляр "Другої", який використовується протягом життєвого циклу тестування. ?
Ананд Хемміге

@AnandHemmige насправді другий (конструктор) є більш чистим, оскільки дозволяє уникнути створення зайвих екземплярів "Second". Ваші заняття так сильно відокремлюються.
душек-перевірка

10
Mockito надає декілька приємних приміток, які дозволяють вводити ваші макети в приватні змінні. Анотувати Друге за допомогою @Mockта коментувати Перше за допомогою @InjectMocksі інстанціювати Перше в ініціалізаторі. Mockito автоматично зробить найкраще знайти місце для введення другого макета в Перший екземпляр, включаючи встановлення приватних полів, що відповідають типу.
jhericks

@Mockбула приблизно в 1,5 (можливо, раніше, я не впевнений). 1.8.3 введено @InjectMocks, а також @Spyі @Captor.
jhericks

7

Якщо ви не можете змінити змінну члена, то навпаки можна використовувати powerMockit та зателефонувати

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

Тепер проблема полягає в тому, що будь-який виклик до нового Second поверне той самий глузливий екземпляр. Але у вашому простому випадку це спрацює.


6

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

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


Не могли б ви пояснити свій код фактичним кодом використання? як публічний клас tobeMocker () {private ClassObject classObject; } Де classObject дорівнює об'єкту, який потрібно замінити.
Джаспер Ланкхорст

У вашому прикладі, якщо екземпляр ToBeMocker = новий ToBeMocker (); і ClassObject someNewInstance = новий ClassObject () {@Override // щось на зразок зовнішньої залежності}; потім TestUtils.refelctSetValue (екземпляр, "classObject", someNewInstance); Зауважте, що ви повинні розібратися, що ви хочете перекрити для глузування. Скажімо, у вас є база даних, і це перевизначення поверне значення, тому не потрібно вибирати. Зовсім недавно у мене була службова шина, яку я не хотів насправді обробляти повідомлення, але хотів переконатися, що воно отримало його. Таким чином, я встановив примірник приватного автобуса таким чином - Корисним?
Дейв

Вам доведеться уявити, що в цьому коментарі було форматування. Його зняли. Крім того, це не буде працювати з Java 9, оскільки заблокує приватний доступ. Нам доведеться співпрацювати з деякими іншими конструкціями, коли ми маємо офіційний реліз і зможемо працювати в межах його фактичних меж.
Дейв

1

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

Якщо ви не можете змінити код, щоб зробити його більш випробуваним, PowerMock: https://code.google.com/p/powermock/

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

Ви використовуєте інший Mock runner. І вам потрібно підготувати клас, який збирається викликати конструктор. (Зауважте, що це звичайний gotcha - підготуйте клас, який викликає конструктор, а не сконструйований клас)

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

Потім у вашій тестовій настройці ви можете використовувати метод WhenNew, щоб конструктор повернув макет

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

0

Так, це можна зробити, як показує наступний тест (написаний з глузливим API JMockit, який я розробляю):

@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, де створюється підклас класу, який слід знущатися; лише екземпляри цього "макетного" підкласу можуть насміхатися з поведінкою, тому потрібно мати тестований код використовувати їх замість будь-якого іншого екземпляра.


3
питання полягало не в тому, чи кращий JMockit від Mockito, а в тому, як це зробити в Mockito. Дотримуйтесь побудови кращого продукту, а не шукайте можливості, щоб зруйнувати конкуренцію!
TheZuck

8
Оригінальний плакат говорить лише про те, що він використовує Mockito; мається на увазі лише те, що Mockito є твердою і жорсткою вимогою, тому натяк на те, що JMockit може впоратися з цією ситуацією, не є таким доцільним.
Бомбе

0

Якщо ви хочете альтернативу ReflectionTestUtils з Spring у макеті, використовуйте

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

Ласкаво просимо до переповнення стека! Є й інші відповіді, які забезпечують питання ОП, і вони були опубліковані багато років тому. Опублікувавши відповідь, будь ласка, переконайтесь, що ви додали або нове рішення, або істотно краще пояснення, особливо, відповідаючи на більш старі питання або коментуючи інші відповіді.
help-info.de
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.