Mockito, JUnit та Spring


77

Я дізнався про Mockito лише сьогодні. Я написав кілька простих тестів (з JUnit, див. Нижче), але я не можу зрозуміти, як я можу використовувати макетний об'єкт у керованих компонентах Spring. Які найкращі практики роботи з Spring. Як я повинен вводити глумливу залежність до моєї квасолі?

Ви можете пропустити це до повернення до мого запитання .

Перш за все те, що я дізнався. Це дуже хороша стаття Mocks Aren't Stubs, яка пояснює основи (Mock перевіряє перевірку поведінки, а не перевірку стану ). Тоді є хороший приклад тут Mockito, а тут Простіше глузування з mockito . У нас є пояснення , що фіктивні об'єкти Mockito є як насмішка і заглушки .

Тут Mockito і тут Matchers ви можете знайти більше прикладів.

Цей тест

@Test
public void testReal(){
    List<String> mockedList = mock(List.class);
     //stubbing
     //when(mockedList.get(0)).thenReturn("first");

    mockedList.get(anyInt());
    OngoingStubbing<String> stub= when(null);
    stub.thenReturn("first");

    //String res = mockedList.get(0);
                //System.out.println(res);

     //you can also verify using argument matcher
     //verify(mockedList).get(anyInt());

    verify(mockedList);
    mockedList.get(anyInt());
}

працює просто чудово.

Поверніться до мого запитання. Тут, вводячи Mockito знущання над Spring bean, хтось намагається використовувати Springs ReflectionTestUtils.setField(), але тут, тут Spring Integration Tests, створюючи Mock Objects, ми маємо рекомендацію змінити контекст Spring.

Я насправді не розумів останніх двох посилань ... Хтось може пояснити мені, яка проблема у Spring з Mockito? Що не так із цим рішенням?

@InjectMocks
private MyTestObject testObject

@Mock
private MyDependentObject mockedObject

@Before
public void setup() {
        MockitoAnnotations.initMocks(this);
}

https://stackoverflow.com/a/8742745/1137529

EDIT : Я насправді не був зрозумілим. Я наведу 3 приклади коду, щоб пояснити себе: Припустимо, у нас є компонент HelloWorld з методом printHello()і компонент HelloFacade з методом, sayHelloякий переадресовує виклики методу HelloWorld printHello().

Перший приклад - використання контексту Spring і без власного бігуна, використання ReflectionTestUtils для введення залежностей (DI):

public class Hello1Test  {
private ApplicationContext ctx;

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
    this.ctx = new ClassPathXmlApplicationContext("META-INF/spring/ServicesImplContext.xml");
}



@Test
public void testHelloFacade() {
    HelloFacade obj = (HelloFacade) ctx.getBean(HelloFacadeImpl.class);
    HelloWorld mock = mock(HelloWorld.class);
    doNothing().when(mock).printHello();

    ReflectionTestUtils.setField(obj, "hello", mock);
    obj.sayHello();

    verify(mock, times(1)).printHello();
}

}

Як зазначив @Noam, існує спосіб запустити його без явного виклику MockitoAnnotations.initMocks(this);. Я також відмовлюся від використання контексту Spring на цьому прикладі.

@RunWith(MockitoJUnitRunner.class)
public class Hello1aTest {


@InjectMocks
private HelloFacade obj =  new HelloFacadeImpl();

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Ще один спосіб зробити це

public class Hello1aTest {

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}


@InjectMocks
private HelloFacadeImpl obj;

@Mock
private HelloWorld mock;


@Test
public void testHelloFacade() {
    doNothing().when(mock).printHello();
    obj.sayHello();
}

}

Нічого, що в попередньому прикладі ми повинні вручну інстанувати HelloFacadeImpl і призначити його HelloFacade, оскільки HelloFacade є інтерфейсом. В останньому прикладі ми можемо просто оголосити HelloFacadeImpl, і Mokito створить його для нас. Недоліком цього підходу є те, що зараз блок тестування є імпл-класом, а не інтерфейсом.


1
Є чи що - то не так з рішенням? Допис у блозі, на який ви посилаєтесь, не використовується @InjectMocks(порівняно недавно, хоча до цього повідомлення в блозі IIRC), тому існують обставини, коли може знадобитися переупорядкування визначень бобів. Я не впевнений, у чому, зрештою, питання.
Дейв Ньютон,

Весна не має проблем з Mockito. Або навпаки.
Роб Кілті,

Я вважаю, що в більшості випадків вам ПОВИННО перевірити фактичну реалізацію замість інтерфейсу.
Адріан Шум,

Я відкрив нове питання з цього питання stackoverflow.com/questions/10937763 / ...
alexsmail

Відповіді:


55

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

По-перше, у більшості випадків НЕ слід турбуватися про весну. Вам рідко потрібно брати участь весни для написання вашого модульного тесту. У звичайному випадку вам потрібно лише створити екземпляр системи, що перевіряється (SUT, ціль, що перевіряється) у вашому модульному тесті, і також ввести залежності SUT у тест. Залежності, як правило, макет / заглушка.

Ваш оригінальний запропонований спосіб і приклади 2, 3 - це саме те, що я описав вище.

У деяких рідкісних випадках (наприклад, тести інтеграції або деякі спеціальні модульні тести) вам потрібно створити контекст програми Spring та отримати SUT із контексту програми. У такому випадку, я вважаю, ви можете:

1) Створіть свій SUT у весняному додатку ctx, отримайте посилання на нього та вводьте знущання

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Before
    /* Initialized mocks */
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void someTest() {
         // ....
    }
}

або

2) дотримуйтесь способу, описаного у вашому посиланні Весняні інтеграційні тести, створюючи макетні об’єкти . Цей підхід полягає у створенні макетів у контексті програми Spring, і ви можете отримати макетний об'єкт з додатка ctx, щоб виконати вашу заглушку / перевірку:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @Autowired
    TestTarget sut;

    @Autowired
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

Обидва шляхи повинні працювати. Основна відмінність полягає в тому, що в першому випадку будуть вводитися залежності після проходження життєвого циклу пружини тощо (наприклад, ініціалізація компонента), а в другому - заздалегідь. Наприклад, якщо ваш SUT реалізує spring's InitializingBean, а процедура ініціалізації включає залежності, ви побачите різницю між цими двома підходами. Я вважаю, що для цих двох підходів немає жодного правильного чи неправильного, якщо ви знаєте, що робите.

Просто доповнення, @Mock, @Inject, MocktoJunitRunner тощо не потрібні у використанні Mockito. Вони є лише утилітами, щоб заощадити набравши Mockito.mock (Foo.class) та купу викликів сетерів.


@Noam, погляньте на цю відповідь. Адріане, чудова відповідь спасибі. :-)
alexsmail

2
Я не думаю, що 1) працює. Тобто: при використанні обох @Autowiredі @InjectMocks, я бачу, як вводиться навесні квасоля, а не насмішки TestTarget. (Я сподівався, що використання обох анотацій призведе лише до висміювання Foo, але все-таки використовувати стандартні введені Spring-компоненти для всіх інших залежностей, які автоматично підключені, TestTargetале не висміюються в тесті інтеграції. Здається, сигари немає. Весна 3.1.2; Mockito 1.9.5)
Аржан

Хоча я цього не пробував, я вважаю, що це має спрацювати. @Autowiredобробляється Spring JUnit Runner, який оброблявся раніше setup(). @InjectMocksобробляється MockitoAnnotaitons.initMocks(this)в setup(). Я не бачу жодної причини, щоб зупинити його роботу. Можливо, я спробую це перевірити, чи працює. :)
Адріан Шум,

7
Я включив MockitoAnnotations.initMocks(this)до @Before. Крім того, видалення @Autowired(отже, лише залишення @InjectMocksна місці) дає мені глузувати TestTarget. (Але видалення @Autowiredтакож залишає всі інші квасолі неініціалізованими до весни.) Однак ще деякі дослідження показують, що мені потрібен TestTarget#setFoo(Foo f)метод. Без цього @InjectMocksпрацює чудово, якщо не поєднуватися з @Autowired. Отже: при використанні обох @Autowiredі @InjectMocks, тоді @Autowired private Foo mockFoo;недостатньо. Можливо, потрібен звіт про помилку; буде розслідувати.
Arjan

1
На жаль, це має значення, якщо хтось використовує@InjectMocks TestTarget sut або @Autowired @InjectMocks TestTarget sut. Для першого Spring не вводить нічого TestTarget(як очікувалося), і досить, якщо TestTargetвизначає приватні властивості, щоб Mockito вводив свої боби. Для останнього TestTargetпотрібен загальнодоступний сетер для кожної квасолі, яку повинен вводити Mockito. Використання останнього для мети , яка ніяк НЕ визначає суспільний Прісваіватель Fooпросто отримує мене весна вводила боб в TestTarget, де весна-впорскується Fooв НЕ замінюється знущатися Mockito.
Arjan

6

Здається, ваше питання задає питання про те, який із трьох прикладів, який ви навели, є найкращим підходом.

Приклад 1 із використанням Reflection TestUtils - не найкращий підхід для модульного тестування . Ви дійсно не хочете взагалі завантажувати контекст spring для модульного тесту. Просто знущайтеся і вводьте те, що потрібно, як показано в інших прикладах.

Ви хочете завантажити контекст spring, якщо хочете провести тестування інтеграції , однак я б віддав перевагу використанню @RunWith(SpringJUnit4ClassRunner.class)для завантаження контексту разом з тим, @Autowiredякщо вам потрібен доступ до його 'бінів явно.

Приклад 2 є допустимим підходом, і використання @RunWith(MockitoJUnitRunner.class)буде усунути необхідність вказувати метод @Before та явний викликMockitoAnnotations.initMocks(this);

Приклад 3 - ще один дійсний підхід, який не використовується @RunWith(...). Ви не створили екземпляр свого класу під тестом HelloFacadeImpl, але ви могли б зробити те саме з прикладом 2.

Я пропоную скористатися прикладом 2 для модульного тестування, оскільки це зменшує безлад у коді. Ви можете повернутися до більш детальної конфігурації, якщо і коли вас змусять це зробити.


4

Введення деяких нових засобів тестування навесні 4.2.RC1 дозволяє писати тести інтеграції Spring, які не покладаються на SpringJUnit4ClassRunner. Перевірте це частину документації.

У вашому випадку ви можете написати свій тест інтеграції Spring і все одно використовувати такі макети:

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration("test-app-ctx.xml")
public class FooTest {

    @ClassRule
    public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();

    @Autowired
    @InjectMocks
    TestTarget sut;

    @Mock
    Foo mockFoo;

    @Test
    public void someTest() {
         // ....
    }
}

1
Тут youtube.com/watch?v=-_aWK8T_YMI можна побачити відео про Spring Framework на Java 8.
alexsmail

2

Вам не потрібно, MockitoAnnotations.initMocks(this);якщо ви використовуєте mockito 1.9 (або новішу) - все, що вам потрібно, це:

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

@InjectMocksАнотацій буде вводити все знущається до MyTestObjectоб'єкту.


10
Саме його використання @RunWith(MockitoJUnitRunner.class)позбавляє потреби телефонуватиMockitoAnnotations.initMocks(this) явно . Як описано тут, анотація доступна з 1.8.3
Бред

1
@Brad Біг з бігуном Mockito не завжди можливий, однак, це все.
Dave Newton

2

Ось мій короткий підсумок.

Якщо ви хочете написати модульний тест, не використовуйте Spring applicationContext, оскільки ви не хочете, щоб у клас, який ви тестуєте, вводились реальні залежності. Натомість використовуйте макети або з @RunWith(MockitoJUnitRunner.class)анотацією поверх класу, або зMockitoAnnotations.initMocks(this) методом @Before.

Якщо ви хочете написати інтеграційний тест, використовуйте:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("yourTestApplicationContext.xml")

Наприклад, щоб налаштувати контекст програми за допомогою бази даних в пам'яті. Зазвичай ви не використовуєте припущення в тестах інтеграції, але ви можете зробити це за допомогоюMockitoAnnotations.initMocks(this) підхід, описаний вище.


0

Різниця в тому, чи потрібно створювати екземпляр @InjectMocksкоментованого поля, полягає у версії Mockito, а не в тому, чи використовуєте ви MockitoJunitRunner або MockitoAnnotations.initMocks. У версії 1.9, яка також буде обробляти деякі введення вашого конструктора@Mock полів, він зробить інстанцію за вас. У попередніх версіях ви повинні створити його самостійно.

Ось як я роблю юніт-тестування моєї весняної квасолі. Немає ніяких проблем. Люди стикаються з плутаниною, коли хочуть використовувати конфігураційні файли Spring, щоб фактично зробити ін'єкцію макетів, що перетинає суть модульних тестів та інтеграційних тестів.

І звичайно одиницею тестування є Impl. Вам потрібно випробувати справжню конкретну річ, так? Навіть якщо ви оголосили його як інтерфейс, вам доведеться створити екземпляр реального, щоб перевірити його. Тепер ви можете потрапити до шпигунів, які є обгортками заглушок / макетів навколо реальних предметів, але це має бути для кутових випадків.


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

Але що виконує "контракт"? Impl, так? Отже, це те, що ви перевіряєте. Як би виглядало тестувати лише інтерфейс? Ви не можете створити екземпляр інтерфейсу.
jhericks

Я відкрив нове питання з цього питання stackoverflow.com/questions/10937763 / ...
alexsmail

Добре, з вашого іншого запитання видно, що ми просто плутали один одного словниковим запасом чи ще чимсь. Звичайно, екземпляр, який ви тестуєте, - це Impl, але чи оголошуєте ви це як інтерфейс чи Impl? Я думаю, що це контекст, але це, безумовно, менш безглуздо, ніж те, що я думав, що ви спочатку запитували.
jhericks

0

Якщо ви хочете перенести свій проект на Spring Boot 1.4, ви можете використовувати нову анотацію @MockBeanдля підробки MyDependentObject. За допомогою цієї функції ви можете видалити Mockito @Mockта @InjectMocksанотації з тесту.

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