«1. Введение

В этой статье мы выйдем за рамки основ JMockit и начнем рассматривать некоторые расширенные сценарии, такие как:

    Подделка (или MockUp API) Утилитарный класс Deencapsulation чем один интерфейс, используя только один макет Как повторно использовать ожидания и проверки

Если вы хотите узнать об основах JMockit, ознакомьтесь с другими статьями из этой серии. Вы можете найти соответствующие ссылки в нижней части страницы.

2. Зависимость Maven

Во-первых, нам нужно добавить зависимость jmockit в наш проект:

<dependency> 
    <groupId>org.jmockit</groupId> 
    <artifactId>jmockit</artifactId> 
    <version>1.41</version>
</dependency>

Далее мы продолжим с примерами.

3. Имитация приватных методов/внутренних классов

Имитация и тестирование приватных методов или внутренних классов часто не считается хорошей практикой.

Причина этого в том, что, если они частные, их не следует тестировать напрямую, поскольку они являются самой внутренней частью класса, но иногда это все же необходимо сделать, особенно при работе с устаревшим кодом.

С JMockit у вас есть два варианта обработки:

    MockUp API для изменения реальной реализации (для второго случая) Вспомогательный класс 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

JMockit’s Mockup API обеспечивает поддержку создания поддельных реализаций или макетов. Как правило, макет нацелен на подделку нескольких методов и/или конструкторов в классе, оставляя большинство других методов и конструкторов без изменений. Это позволяет полностью переписать класс, поэтому любой метод или конструктор (с любым модификатором доступа) может быть нацелен.

Давайте посмотрим, как мы можем переопределить 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. Использование класса Deencapsulation

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. Повторное использование ожиданий и проверок

В конце концов, при тестировании классов вы можете столкнуться со случаями повторения одних и тех же ожиданий и/или проверок снова и снова. Чтобы облегчить это, вы можете легко повторно использовать оба.

@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);
    }
}

Мы собираемся объяснить это на примере (мы используем классы Model, Collaborator и Performer из нашей статьи JMockit 101):

В этом примере вы можете видеть в строках с 15 до 18, что мы готовим ожидание для каждого теста, чтобы model.getInfo() всегда возвращал «foo», а Collaborator.collaborate() всегда ожидал «foo» в качестве аргумента и возвращал true. Мы помещаем оператор minTimes = 0, чтобы не возникало сбоев, когда они фактически не использовались в тестах.

Кроме того, мы создали метод verifyTrueCalls(int), чтобы упростить проверки метода collaborator.receive(boolean), когда переданный аргумент истинен.

Наконец, вы также можете создавать новые типы определенных ожиданий и проверок, просто расширяя любой из классов Expectations или Verifications. Затем вы определяете конструктор, если вам нужно настроить поведение и создать новый экземпляр указанного типа в тесте, как мы это делаем в строках с 33 по 43.

6. Заключение

В этом выпуске серии JMockit мы затронули несколько продвинутых тем, которые определенно помогут вам в повседневных насмешках и тестировании.

Мы можем написать больше статей о JMockit, так что следите за обновлениями, чтобы узнать больше.

И, как всегда, полную реализацию этого руководства можно найти на GitHub.

6.1. Статьи серии

    Все статьи серии: