Порядок вызовов, статические методы и конструкторы
В этом уроке — три более редких инструмента Mockito. InOrder проверяет не только то, что вызовы были, но и в каком порядке. mockStatic подменяет статический метод на время теста — например, LocalDate.now(), который каждый день возвращает новую дату. mockConstruction перехватывает new. Последние два — инструменты на крайний случай: в конце посмотрим, как сделать код тестируемым без них.
InOrder
publish() должен сначала сохранить пост и только потом отправить письмо — иначе автор может получить уведомление о посте, который не сохранился. Обычный verify порядка не видит, а InOrder видит:
// src/test/java/com/example/blog/PostServiceInOrderTest.java
package com.example.blog;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
class PostServiceInOrderTest {
private final PostRepository repository = mock(PostRepository.class);
private final Notifier notifier = mock(Notifier.class);
private final PostService service = new PostService(repository, notifier);
@Test
void savesBeforeNotifying() {
when(repository.findById(1L))
.thenReturn(Optional.of(new Post(1L, "Привет", "anna", false)));
service.publish(1L);
InOrder inOrder = inOrder(repository, notifier);
inOrder.verify(repository).save(any(Post.class));
inOrder.verify(notifier).send(anyString(), anyString());
}
}inOrder(...) принимает моки, вызовы которых сравниваются. Проверки verify через него должны совпасть в том же порядке, что и в коде. Если поменять две строки местами, тест упадёт:
org.mockito.exceptions.verification.VerificationInOrderFailure:
Verification in order failure
Wanted but not invoked:
postRepository.save(
<any com.example.blog.Post>
);
-> at com.example.blog.InOrderFailTest.notifiesBeforeSaving(InOrderFailTest.java:27)
Wanted anywhere AFTER following interaction:
notifier.send(
"anna",
"Ваш пост «Привет» опубликован"
);Проверяйте порядок только там, где он важен для поведения, как здесь.
mockStatic
Магазин даёт скидку 10% в выходные. Класс сам узнаёт дату:
// src/main/java/com/example/shop/WeekendDiscount.java
package com.example.shop;
import java.time.DayOfWeek;
import java.time.LocalDate;
public class WeekendDiscount {
public int percent() {
DayOfWeek day = LocalDate.now().getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY ? 10 : 0;
}
}Тест такого кода проходит в субботу и падает в понедельник. mockStatic подменяет LocalDate.now() (для этого нужен inline mock maker — он по умолчанию в Mockito 5, а агент настроен в первом уроке о Mockito):
// src/test/java/com/example/shop/WeekendDiscountStaticTest.java
package com.example.shop;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mockStatic;
class WeekendDiscountStaticTest {
@Test
void tenPercentOnSaturday() {
LocalDate saturday = LocalDate.of(2026, 10, 3);
try (MockedStatic<LocalDate> mocked = mockStatic(LocalDate.class)) {
mocked.when(LocalDate::now).thenReturn(saturday);
assertEquals(10, new WeekendDiscount().percent());
}
}
}Внутри блока try подменены все статические методы LocalDate; при выходе из него try-with-resources вызывает close(), и настоящие методы возвращаются. Второй тест, zeroOnMonday, делает то же самое с понедельником, 5 октября 2026.
Обратите внимание: дата создаётся до блока. Внутри него LocalDate.of(...) тоже подменён и возвращает null, поэтому thenReturn(LocalDate.of(2026, 10, 3)) падает с UnfinishedStubbingException.
mockConstruction
mockConstruction делает так, что каждый new класса внутри блока возвращает мок. Для класса, который сам создаёт калькулятор:
// src/main/java/com/example/shop/Checkout.java
public class Checkout {
public BigDecimal pay(BigDecimal price, int quantity) {
PriceCalculator calculator = new PriceCalculator();
return calculator.total(price, quantity);
}
}// src/test/java/com/example/shop/CheckoutTest.java
@Test
void usesPriceCalculator() {
try (MockedConstruction<PriceCalculator> mocked = mockConstruction(PriceCalculator.class,
(mock, context) -> when(mock.total(any(), anyInt())).thenReturn(new BigDecimal("1.00")))) {
BigDecimal total = new Checkout().pay(new BigDecimal("12.00"), 3);
assertEquals(new BigDecimal("1.00"), total);
assertEquals(1, mocked.constructed().size());
}
}Второй аргумент настраивает каждый созданный мок; constructed() возвращает моки, созданные к этому моменту.
[INFO] Running com.example.shop.CheckoutTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.099 s -- in com.example.shop.CheckoutTest
[INFO] Running com.example.shop.WeekendDiscountStaticTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.138 s -- in com.example.shop.WeekendDiscountStaticTestЛучше: передавать Clock
mockStatic и mockConstruction прячут проблему дизайна: класс сам добывает свои зависимости. Передайте их снаружи — и никакие трюки не понадобятся. Для времени в Java есть java.time.Clock:
// src/main/java/com/example/shop/WeekendDiscount.java
public class WeekendDiscount {
private final Clock clock;
public WeekendDiscount(Clock clock) {
this.clock = clock;
}
public int percent() {
DayOfWeek day = LocalDate.now(clock).getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY ? 10 : 0;
}
}// src/test/java/com/example/shop/WeekendDiscountTest.java
@Test
void tenPercentOnSaturday() {
Clock saturday = Clock.fixed(Instant.parse("2026-10-03T12:00:00Z"), ZoneOffset.UTC);
assertEquals(10, new WeekendDiscount(saturday).percent());
}Clock.fixed всегда показывает заданный момент. Приложение передаёт Clock.systemDefaultZone(), тест — фиксированные часы, и моки вообще не нужны. Точно так же Checkout мог бы получать PriceCalculator через конструктор.
Частая ошибка
mockStatic без try-with-resources. Статический мок остаётся активным после теста, и следующий тест в том же потоке не может создать свой:
MockedStatic<LocalDate> mocked = mockStatic(LocalDate.class);
mocked.when(LocalDate::now).thenReturn(saturday);[ERROR] com.example.shop.StaticNoCloseTest.zeroOnMonday -- Time elapsed: 0.015 s <<< ERROR!
org.mockito.exceptions.base.MockitoException:
For java.time.LocalDate, static mocking is already registered in the current thread
To create a new mock, the existing static mock registration must be deregisteredХуже того, до этого момента все остальные тесты видят поддельную дату. Всегда открывайте MockedStatic и MockedConstruction в try (...) — или вызывайте close() в @AfterEach.