Як працює макет, коли () виклик працює?


111

З огляду на таке твердження Mockito:

when(mock.method()).thenReturn(someValue);

Як Mockito збирається створити проксі-сервер для макета, враховуючи, що оператор mock.method () передасть значення, що повертається, коли ()? Я думаю, що для цього використовуються деякі речі CGLib, але мені було б цікаво дізнатись, як це робиться технічно.

Відповіді:


118

Коротка відповідь полягає в тому, що у вашому прикладі результатом mock.method()буде порожнє значення, відповідне типу; mockito використовує опосередкування за допомогою проксі, методу перехоплення та спільного екземпляра MockingProgressкласу для того, щоб визначити, чи викликання методу в макеті є для заглушки або перетворення наявної заглушеної поведінки, а не передачі інформації про заглушку через повернене значення глузливий метод.

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

По-перше, коли ви знущаєтесь над класом, використовуючи mockметод Mockitoкласу, це по суті відбувається:

  1. Mockito.mockделегує до org.mockito.internal.MockitoCore.mock, передаючи параметри макету за замовчуванням як параметр.
  2. MockitoCore.mockделегати org.mockito.internal.util.MockUtil.createMock
  3. MockUtilКлас використовує ClassPathLoaderклас , щоб отримати примірник MockMakerвикористовувати для створення знущатися. За замовчуванням використовується клас CgLibMockMaker .
  4. CgLibMockMakerвикористовує клас, запозичений у JMock, ClassImposterizerякий обробляє створення макета. Ключові фрагменти "магічної макети", які MethodInterceptorвикористовуються для створення макету: мокіто MethodInterceptorFilterта ланцюжок екземплярів MockHandler, включаючи екземпляр MockHandlerImpl . Перехоплювач методів передає виклики екземпляру MockHandlerImpl, який реалізує логіку бізнесу, яку слід застосувати, коли метод викликається на макеті (тобто, пошук, щоб відповідь записано вже, визначення, чи виклик являє собою нову заглушку тощо). Станом за замовчуванням є те, що якщо заглушка ще не зареєстрована для методу, що викликається, повертається відповідне типу порожнє значення.

Тепер давайте розглянемо код у вашому прикладі:

when(mock.method()).thenReturn(someValue)

Ось порядок виконання цього коду в:

  1. mock.method()
  2. when(<result of step 1>)
  3. <result of step 2>.thenReturn

Ключовим моментом для розуміння того, що відбувається, є те, що відбувається, коли викликається метод на макеті: перехоплювач методу передає інформацію про виклик методу та делегує його ланцюгу MockHandlerекземплярів, які в підсумку делегуються MockHandlerImpl#handle. Під MockHandlerImpl#handleчас обробник макетів створює екземпляр OngoingStubbingImplі передає його спільному MockingProgressекземпляру.

Коли whenметод викликається після виклику method(), він делегується до MockitoCore.when, який викликає stub()метод того ж класу. Цей метод розпаковує поточну заглушку із спільного MockingProgressекземпляра, в яку записано method()глузливе виклик, і повертає його. Потім thenReturnметод викликається в OngoingStubbingекземплярі.


1
Дякуємо за детальну відповідь. Ще одне питання - ви згадуєте, що "коли метод викликається після виклику методу ()" - як дізнатися, що виклик, коли () - це наступне виклик (або обгортання) виклику методу ()? Сподіваюся, що це має сенс.
marchaos

@marchaos Це не знає. За допомогою when(mock.method()).thenXyz(...)синтаксису mock.method()виконується в режимі "повтор", а не в режимі "заглушення". Як правило, це виконання mock.method()не має ніякого ефекту, тому пізніше , коли thenXyz(...)( thenReturn, thenThrow, thenAnswerі т.д.) запускається на виконання, він переходить в «гасячи» режим , а потім записує бажаний результат для цього виклику методу.
Rogério

1
Роджеріо, це насправді трохи тонкіше, ніж це - у мокіто немає явних режимів заготівлі та повторного відтворення. Свою відповідь я відредагую пізніше, щоб вона була більш зрозумілою.
Пол Морі

Я підсумую, що, коротше кажучи, простіше перехопити виклик методу в іншому методі з CGLIB або Javassist, що перехоплює, скажімо, оператор "якщо".
Infeligo

Я ще не масував свій опис тут, але й не забув про це. FYI.
Пол Морі

33

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

Цю статтю я вважаю дуже корисною: Пояснення роботи Mock Frameworks на основі проксі ( http://blog.rseiler.at/2014/06/explanation-how-proxy-based-mock.html ). Автор реалізував демонстраційний фреймворк Mocking, який знайшов дуже хороший ресурс для людей, які хочуть зрозуміти, як працюють ці рамки Mocking.

На мою думку, це типове використання Anti-Pattern. Зазвичай ми повинні уникати "побічного ефекту", коли ми реалізуємо метод, тобто метод повинен приймати введення та робити якийсь розрахунок і повертати результат - крім цього нічого більше не змінилося. Але Мокіто навмисно порушує це правило. Його методи зберігають купу інформації, окрім повернення результату: Mockito.anyString (), mockInstance.method (), коли (), то повернути, всі вони мають особливий "побічний ефект". Ось чому фреймворк на перший погляд виглядає як магія - ми зазвичай не пишемо такий код. Однак у глузливій рамковій справі цей антидіапазонний дизайн є чудовим дизайном, оскільки призводить до дуже простого API.


4
Відмінне посилання. Геній, що стоїть за цим, полягає в тому, що: дуже простий API, завдяки якому все це виглядає дуже приємно. Ще одне чудове рішення полягає в тому, що метод When () використовує дженерики, щоб метод thenReturn () був безпечним для типу.
Девід Тонхофер

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