Як працюють матчери Mockito?


122

Mockito аргумент matchers (наприклад any, argThat, eq, sameі ArgumentCaptor.capture()) поводяться дуже по- різному від Hamcrest matchers.

  • Матчі-відповідники часто викликають InvalidUseOfMatchersException, навіть у коді, який виконується довго після того, як використовувалися будь-які відповідники.

  • Макетні відповідники дотримуються дивних правил, таких як лише використання Mockito Matchers для всіх аргументів, якщо в одному аргументі в даному методі використовується матчер.

  • Mockito відповідники можуть викликати NullPointerException при переопределенні Answers або при використанні (Integer) any()і т.д.

  • Код рефакторингу з відповідниками Mockito певними способами може спричинити винятки та несподівану поведінку, а також може повністю вийти з ладу.

Чому матриці Mockito розроблені так і як вони реалізовані?

Відповіді:


236

Матчі-відповідники - це статичні методи та виклики до тих методів, які є аргументами під час викликів до whenта verify.

Матчі Hamcrest (архівована версія) (або відповідники стилю Hamcrest) - це об'єкти без об'єкта, без загального призначення, які реалізують Matcher<T>та розкривають метод, matches(T)який повертає істину, якщо об'єкт відповідає критеріям Матчера. Вони призначені для позбавлення від побічних ефектів, і, як правило, використовуються в таких твердженнях, як наведене нижче.

/* Mockito */  verify(foo).setPowerLevel(gt(9000));
/* Hamcrest */ assertThat(foo.getPowerLevel(), is(greaterThan(9000)));

Матчі-відповідники існують окремо від відповідних стилів Hamcrest, так що описи відповідних виразів вписуються безпосередньо у виклики методів : Матчі-макіто повертаються Tтуди, де методики Матчреста повертають об'єкти Матчер (типу Matcher<T>).

Mockito matchers викликаються через статичні методи , такі як eq, any, gt, і startsWithна org.mockito.Matchersі org.mockito.AdditionalMatchers. Є також адаптери, які змінилися у всіх версіях Mockito:

  • Для Mockito 1.x Matchersдеякі дзвінки (наприклад, intThatабо argThat) - відповідники Mockito, які безпосередньо приймають відповідники Hamcrest як параметри. ArgumentMatcher<T>розширений org.hamcrest.Matcher<T>, який використовувався у внутрішньому представленні Hamcrest і був базовим класом Hamcrest matcher замість будь-якого типу Mockito matcher.
  • Для Mockito 2.0+ Mockito більше не має прямої залежності від Hamcrest. Matchersназиває фразовані як intThatабо argThatобгортання ArgumentMatcher<T>об'єктів, які більше не реалізуються, org.hamcrest.Matcher<T>але використовуються аналогічно. Адаптери Hamcrest, такі як argThatі intThatвсе ще доступні, але MockitoHamcrestзамість цього перейшли .

Незалежно від того, чи є матчі в стилі Hamcrest чи просто в Hamcrest, вони можуть бути адаптовані так:

/* Mockito matcher intThat adapting Hamcrest-style matcher is(greaterThan(...)) */
verify(foo).setPowerLevel(intThat(is(greaterThan(9000))));

У наведеному вище твердженні: foo.setPowerLevelце метод, який приймає int. is(greaterThan(9000))повертає a Matcher<Integer>, який би не працював як setPowerLevelаргумент. Матчі Mockito intThatобгортає відповідник стилю Hamcrest і повертає intтак, що це може з'явитися як аргумент; Матчі-відповідники, як-от, перетворили gt(9000)б цілий вираз у один виклик, як у першому рядку прикладу коду.

Що матчі роблять / повертають

when(foo.quux(3, 5)).thenReturn(true);

Не використовуючи відповідники аргументів, Mockito записує ваші значення аргументів і порівнює їх зі своїми equalsметодами.

when(foo.quux(eq(3), eq(5))).thenReturn(true);    // same as above
when(foo.quux(anyInt(), gt(5))).thenReturn(true); // this one's different

Коли ви викликаєте відповідний матч anyабо gt(більше, ніж), Mockito зберігає об'єкт відповідності, який змушує Mockito пропустити цю перевірку рівності і застосувати ваш вибір відповідності. У випадку, коли argumentCaptor.capture()він зберігає матч, який зберігає його аргумент замість для подальшої перевірки.

Матчі повертають фіктивні значення, такі як нуль, порожні колекції або null. Mockito намагається повернути безпечне, відповідне фіктивне значення, наприклад 0 для anyInt()або any(Integer.class)порожнє List<String>для anyListOf(String.class). З - за типу стирання, хоча, Mockito не вистачає інформації про типі , щоб повернути будь-яке значення , але nullдля any()або argThat(...), що може привести до NullPointerException , якщо намагатися «авто-Unbox» а nullпримітивне значення.

Матчі люблять eqі gtприймають значення параметрів; в ідеалі ці значення повинні бути обчислені до початку заглушки / перевірки. Виклик макету посеред глузування іншого дзвінка може заважати заглушувати.

Методи відповідника не можна використовувати як повернені значення; немає ніякого способу вираження thenReturn(anyInt())або thenReturn(any(Foo.class))в Mockito, наприклад. Mockito повинен точно знати, який екземпляр повернути в заглушених викликах, і не вибере для вас довільне значення повернення.

Деталі реалізації

Матчі зберігаються (як відповідні об'єкти стилю Hamcrest) у стеці, що міститься в класі, який називається ArgumentMatcherStorage . MockitoCore та Matchers мають власний екземпляр ThreadSafeMockingProgress , який статично містить екземпляри MockingProgress , які містять ThreadLocal. Саме цей MockingProgressImpl містить конкретний ArgumentMatcherStorageImpl . Отже, стан макетів та матчерів є статичним, але послідовно розподіляється між класами Mockito та Matchers.

Більшість дзвінків Слічітель тільки додати в цей стек, з виключенням для matchers , як and, orіnot . Це абсолютно відповідає (і покладається на) порядок оцінки Java , який оцінює аргументи зліва направо перед викликом методу:

when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6]      [5]  [1]       [4] [2]     [3]

Це буде:

  1. Додати anyInt()в стек.
  2. Додати gt(10)в стек.
  3. Додати lt(20)в стек.
  4. Видалити gt(10)і lt(20)додати and(gt(10), lt(20)).
  5. Дзвінок foo.quux(0, 0), який (якщо не вказано інше) повертає значення за замовчуванням false. Мокіто внутрішньо позначається quux(int, int)як останній дзвінок.
  6. Call when(false), який відкидає свій аргумент і готується до методу stub, quux(int, int)визначеного у 5. Єдині два дійсних стани мають довжину стека 0 (рівність) або 2 (відповідники), а на стеці є два відповідники (кроки 1 і 4), Mockito закріплює метод за допомогою any()матчера для першого аргументу та and(gt(10), lt(20))другого аргументу та очищує стек.

Це демонструє кілька правил:

  • Мокіто не може визначити різницю між quux(anyInt(), 0)та quux(0, anyInt()). Вони обидва виглядають як дзвінок до quux(0, 0)одного int matcher на стеку. Отже, якщо ви використовуєте один матч, ви повинні відповідати всім аргументам.

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

    int between10And20 = and(gt(10), lt(20));
    /* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
    // Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
    
    public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
    /* OK */  when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
    // The helper method calls the matcher methods in the right order.
  • Стек змінюється досить часто, що Mockito не може дуже обережно його поліціювати. Він може перевірити стек лише тоді, коли ви взаємодієте з Mockito або макетом, і повинен приймати відповідники, не знаючи, чи використовуються вони негайно або відмовилися випадково. Теоретично стек повинен бути порожнім поза викликом до whenабо verify, але Mockito не може перевірити це автоматично. Ви можете перевірити вручну за допомогою Mockito.validateMockitoUsage().

  • У заклику до whenMockito насправді викликає розглянутий метод, який викине виняток, якщо ви змусили метод викинути виняток (або потребуєте ненульових чи ненульових значень). doReturnта doAnswer(тощо) не використовують фактичний метод і часто є корисною альтернативою.

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

  • Якщо ви спробуєте зробити щось погане, наприклад , заглушку / перевірку остаточного методу , Mockito зателефонує до справжнього методу, а також залишить додаткові відповідники на стеці . finalВиклик методи не може кинути виняток, але ви можете отримати InvalidUseOfMatchersException від паразитного matchers , коли ви в наступний раз взаємодіяти з макетом.

Поширені проблеми

  • InvalidUseOfMatchersException :

    • Переконайтеся, що кожен аргумент має рівно один виклик відповідника, якщо ви взагалі використовуєте матчі та чи не використовували ви матч поза whenабо verifyвикликом. Матчі ніколи не повинні використовуватися як заглушені зворотні значення або поля / змінні.

    • Переконайтеся, що ви не викликаєте макет як частину надання аргументу відповідника.

    • Переконайтеся, що ви не намагаєтеся заглушити / перевірити остаточний метод за допомогою відповідника. Це прекрасний спосіб залишити матч на стеці, і якщо ваш остаточний метод не викидає виняток, це може бути єдиний раз, коли ви усвідомлюєте, що метод, з якого ви знущаєтесь, остаточний.

  • NullPointerException з примітивними аргументами: (Integer) any() повертає null, а any(Integer.class)повертає 0; це може спричинити, NullPointerExceptionякщо ви очікуєте intзамість цілого числа. У будь-якому випадку віддайте перевагу anyInt(), яка поверне нуль, а також пропустить крок автобоксу.

  • NullPointerException або інші винятки: Виклики when(foo.bar(any())).thenReturn(baz)насправді дзвонять foo.bar(null) , які, можливо, вам доведеться кинути виняток при отриманні аргументу нуля. Перемикання на doReturn(baz).when(foo).bar(any()) пропуск стрибкованої поведінки .

Загальне усунення несправностей

  • Використовуйте MockitoJUnitRunner або явно зателефонуйте validateMockitoUsageу свій метод tearDownчи @Afterметод (який бігун зробить для вас автоматично). Це допоможе визначити, що ви неправомірно використовували відповідники.

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


2
Дякую за цей запис. NullPointerException з форматом коли / thenReturn викликав у мене проблеми, поки я не змінив його на doReturn / when.
yngwietiger

11

Лише невелике доповнення до відмінної відповіді Джеффа Боумена, коли я знайшов це питання під час пошуку вирішення однієї з моїх власних проблем:

Якщо виклик методу відповідає більш ніж одному whenпідготовленому дзвінку, то порядок whenдзвінків є важливим і повинен бути від самого широкого до найбільш конкретного. Починаючи з одного із прикладів Джеффа:

when(foo.quux(anyInt(), anyInt())).thenReturn(true);
when(foo.quux(anyInt(), eq(5))).thenReturn(false);

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

foo.quux(3 /*any int*/, 8 /*any other int than 5*/) //returns true
foo.quux(2 /*any int*/, 5) //returns false

Якщо ви зворотні, коли дзвінки, результат завжди був би true.


2
Хоча це корисна інформація, вона стосується заглушки, а не відповідників , тому це може не мати сенсу в цьому питанні. Порядок має значення, але лише в тому, що перемогла остання визначена відповідна ланцюжок : Це означає, що співіснуючі заглушки часто оголошуються найбільш специфічними як мінімум, але в деяких випадках вам потрібно дуже широкий перелік конкретно-глузливої ​​поведінки в одному тестовому випадку , і тоді широке визначення може знадобитися останнє.
Jeff Bowman

1
@JeffBowman Я подумав, що в цьому питанні є сенс, оскільки питання про макетні матриці, а матчери можуть бути використані при заглушці (як у більшості ваших прикладів). Оскільки пошук google для пояснення привів мене до цього питання, я думаю, що корисно мати тут цю інформацію.
tibtof
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.