1. Введение
В этой статье мы выйдем за рамки основ JMockit и начнем рассматривать некоторые расширенные сценарии, такие как:
- Подделка (или
MockUp
API) Вспомогательный
класс деинкапсуляции- Как издеваться над более чем одним интерфейсом, используя только один макет
- Как повторно использовать ожидания и проверки
Если вы хотите познакомиться с основами JMockit, ознакомьтесь с другими статьями из этой серии. Вы можете найти соответствующие ссылки в нижней части страницы.
2. Зависимость от Maven
Во-первых, нам нужно добавить зависимость jmockit в наш проект:
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.41</version>
</dependency>
Далее мы продолжим с примерами.
3. Насмешка над частными методами/внутренними классами
Насмешка и тестирование частных методов или внутренних классов часто не считается хорошей практикой.
Причина этого в том, что, если они частные, их не следует тестировать напрямую, поскольку они являются самыми внутренними частями класса, но иногда это все же необходимо сделать, особенно при работе с устаревшим кодом.
С JMockit у вас есть два варианта решения этой проблемы:
- API
MockUp
для изменения реальной реализации (для второго случая) Вспомогательный класс Deencapsulation
для прямого вызова любого метода (для первого случая)
Все следующие примеры будут выполнены для следующего класса, и мы предположим, что они выполняются в тестовом классе с той же конфигурацией, что и первый (чтобы избежать повторения кода):
public class AdvancedCollaborator {
int i;
private int privateField = 5;
// default constructor omitted
public AdvancedCollaborator(String string) throws Exception{
i = string.length();
}
public String methodThatCallsPrivateMethod(int i) {
return privateMethod() + i;
}
public int methodThatReturnsThePrivateField() {
return privateField;
}
private String privateMethod() {
return "default:";
}
class InnerAdvancedCollaborator {...}
}
3.1. Подделка с помощью мокапа
Mockup API JMockit обеспечивает поддержку создания поддельных реализаций или мокапов
. Как правило, макет
нацелен на подделку нескольких методов и/или конструкторов в классе, оставляя большинство других методов и конструкторов без изменений. Это позволяет полностью переписать класс, поэтому любой метод или конструктор (с любым модификатором доступа) может быть нацелен.
Давайте посмотрим, как мы можем переопределить privateMethod()
с помощью API Mockup:
@RunWith(JMockit.class)
public class AdvancedCollaboratorTest {
@Tested
private AdvancedCollaborator mock;
@Test
public void testToMockUpPrivateMethod() {
new MockUp<AdvancedCollaborator>() {
@Mock
private String privateMethod() {
return "mocked: ";
}
};
String res = mock.methodThatCallsPrivateMethod(1);
assertEquals("mocked: 1", res);
}
}
В этом примере мы определяем новый MockUp
для класса AdvancedCollaborator , используя аннотацию
@Mock
для метода с соответствующей подписью. После этого вызовы этого метода будут делегированы нашему фиктивному.
Мы также можем использовать это для макета
конструктора класса, которому нужны определенные аргументы или конфигурация для упрощения тестов:
@Test
public void testToMockUpDifficultConstructor() throws Exception{
new MockUp<AdvancedCollaborator>() {
@Mock
public void $init(Invocation invocation, String string) {
((AdvancedCollaborator)invocation.getInvokedInstance()).i = 1;
}
};
AdvancedCollaborator coll = new AdvancedCollaborator(null);
assertEquals(1, coll.i);
}
В этом примере мы видим, что для имитации конструктора вам нужно имитировать метод $init
. Вы можете передать дополнительный аргумент типа Invocation,
с помощью которого вы можете получить доступ к информации о вызове фиктивного метода, включая экземпляр, для которого выполняется вызов.
3.2. Использование класса деинкапсуляции
JMockit включает тестовый служебный класс: Deencapsulation
. Как видно из названия, он используется для деинкапсуляции состояния объекта, и с его помощью вы можете упростить тестирование, получая доступ к полям и методам, к которым иначе получить доступ было бы невозможно.
Вы можете вызвать метод:
@Test
public void testToCallPrivateMethodsDirectly(){
Object value = Deencapsulation.invoke(mock, "privateMethod");
assertEquals("default:", value);
}
Вы также можете установить поля:
@Test
public void testToSetPrivateFieldDirectly(){
Deencapsulation.setField(mock, "privateField", 10);
assertEquals(10, mock.methodThatReturnsThePrivateField());
}
И получить поля:
@Test
public void testToGetPrivateFieldDirectly(){
int value = Deencapsulation.getField(mock, "privateField");
assertEquals(5, value);
}
И создайте новые экземпляры классов:
@Test
public void testToCreateNewInstanceDirectly(){
AdvancedCollaborator coll = Deencapsulation
.newInstance(AdvancedCollaborator.class, "foo");
assertEquals(3, coll.i);
}
Даже новые экземпляры внутренних классов:
@Test
public void testToCreateNewInnerClassInstanceDirectly(){
InnerCollaborator inner = Deencapsulation
.newInnerInstance(InnerCollaborator.class, mock);
assertNotNull(inner);
}
Как видите, класс Deencapsulation
чрезвычайно полезен при тестировании герметичных классов. Одним из примеров может быть установка зависимостей класса, который использует аннотации @Autowired
для частных полей и не имеет для них сеттеров, или внутренние классы модульного тестирования без необходимости зависеть от общедоступного интерфейса своего класса-контейнера.
4. Мокирование нескольких интерфейсов в одном макете
Предположим, вы хотите протестировать класс — еще не реализованный — но точно знаете, что он будет реализовывать несколько интерфейсов.
Обычно вы не сможете протестировать указанный класс перед его реализацией, но с JMockit у вас есть возможность заранее подготовить тесты, имитируя более одного интерфейса с использованием одного фиктивного объекта.
Этого можно добиться, используя дженерики и определяя тип, который расширяет несколько интерфейсов. Этот общий тип может быть определен либо для всего тестового класса, либо только для одного тестового метода.
Например, мы собираемся создать макет для интерфейсов List
и Comparable
двумя способами :
@RunWith(JMockit.class)
public class AdvancedCollaboratorTest<MultiMock
extends List<String> & Comparable<List<String>>> {
@Mocked
private MultiMock multiMock;
@Test
public void testOnClass() {
new Expectations() {{
multiMock.get(5); result = "foo";
multiMock.compareTo((List<String>) any); result = 0;
}};
assertEquals("foo", multiMock.get(5));
assertEquals(0, multiMock.compareTo(new ArrayList<>()));
}
@Test
public <M extends List<String> & Comparable<List<String>>>
void testOnMethod(@Mocked M mock) {
new Expectations() {{
mock.get(5); result = "foo";
mock.compareTo((List<String>) any); result = 0;
}};
assertEquals("foo", mock.get(5));
assertEquals(0, mock.compareTo(new ArrayList<>()));
}
}
Как вы можете видеть в строке 2, мы можем определить новый тип теста для всего теста, используя дженерики для имени класса. Таким образом, MultiMock
будет доступен как тип, и вы сможете создавать для него моки, используя любые аннотации JMockit.
В строках с 7 по 18 мы видим пример использования макета мультикласса, определенного для всего тестового класса.
Если вам нужен макет с несколькими интерфейсами только для одного теста, вы можете добиться этого, определив универсальный тип в сигнатуре метода и передав новый макет этого нового универсального в качестве аргумента метода тестирования. В строках с 20 по 32 мы можем увидеть пример того же проверенного поведения, что и в предыдущем тесте.
5. Повторное использование ожиданий и проверок
В конце концов, при тестировании классов вы можете столкнуться со случаями повторения одних и тех же ожиданий
и/или проверок
снова и снова. Чтобы облегчить это, вы можете легко повторно использовать оба.
Мы собираемся объяснить это на примере (мы используем классы Model, Collaborator
и Performer
из нашей статьи JMockit 101 ):
@RunWith(JMockit.class)
public class ReusingTest {
@Injectable
private Collaborator collaborator;
@Mocked
private Model model;
@Tested
private Performer performer;
@Before
public void setup(){
new Expectations(){{
model.getInfo(); result = "foo"; minTimes = 0;
collaborator.collaborate("foo"); result = true; minTimes = 0;
}};
}
@Test
public void testWithSetup() {
performer.perform(model);
verifyTrueCalls(1);
}
protected void verifyTrueCalls(int calls){
new Verifications(){{
collaborator.receive(true); times = calls;
}};
}
final class TrueCallsVerification extends Verifications{
public TrueCallsVerification(int calls){
collaborator.receive(true); times = calls;
}
}
@Test
public void testWithFinalClass() {
performer.perform(model);
new TrueCallsVerification(1);
}
}
В этом примере вы можете видеть в строках с 15 по 18, что мы готовим ожидание для каждого теста, так что model.getInfo()
всегда возвращает «foo»
, а Collaborator.collaborate
()
всегда ожидает «foo»
в качестве аргумент и возвращает true
. Мы помещаем оператор minTimes = 0
, чтобы не возникало сбоев, когда они фактически не использовались в тестах.
Кроме того, мы создали метод verifyTrueCalls(int)
для упрощения проверки метода collaborator.receive(boolean)
, когда переданный аргумент имеет значение true
.
Наконец, вы также можете создавать новые типы определенных ожиданий и проверок, просто расширяя любой из классов Expectations
или Verifications .
Затем вы определяете конструктор, если вам нужно настроить поведение и создать новый экземпляр указанного типа в тесте, как мы делаем в строках с 33 по 43.
6. Заключение
В этом выпуске серии JMockit мы затронули несколько продвинутых тем, которые определенно помогут вам в повседневном мокировании и тестировании.
Мы можем написать больше статей о JMockit, так что следите за обновлениями, чтобы узнать больше.
И, как всегда, полную реализацию этого руководства можно найти на GitHub.
6.1. Статьи в серии
Все статьи цикла: