Расширенное использование JMockit

1. Вступление

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

  • Подделка (или API MockUp )

  • Служебный класс Deencapsulation

  • Как смоделировать более одного интерфейса, используя только один макет

  • Как повторно использовать ожидания и проверки

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

2. Частные методы/Внутренние классы, издевающиеся

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

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

С 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 {...}
}

2.1. Подделка с помощью MockUp

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

Давайте посмотрим, как мы можем переопределить 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 для метода с соответствующей сигнатурой. После этого вызовы этого метода будут делегированы нашему поддельному.

Мы также можем использовать это, чтобы mock-up конструктор класса, который нуждается в определенных аргументах или конфигурации для упрощения тестов:

@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 , с помощью которого вы можете получить доступ к информации о вызове смоделированного метода, включая экземпляр, для которого выполняется вызов.

2.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 для частных полей и не имеет для них установщиков, или для модульного тестирования внутренних классов без необходимости зависеть от открытого интерфейса своего класса контейнера.

3. Пересмешивание нескольких интерфейсов в одной и той же насмешке

Давайте предположим, что вы хотите протестировать класс - еще не реализованный - но вы точно знаете, что он будет реализовывать несколько интерфейсов.

Обычно вы не сможете протестировать указанный класс перед его реализацией, но с 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 мы можем видеть пример того же поведения, что и в предыдущем тесте.

4. Повторное использование ожиданий и проверок

В конце концов, при тестировании классов вы можете столкнуться со случаями, когда вы повторяете одни и те же Expectations и/или Verifications снова и снова.

Чтобы облегчить это, вы можете легко использовать оба.

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

@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 или Verification . Затем вы определяете конструктор, если вам нужно настроить поведение и создать новый экземпляр указанного типа в тесте, как мы делаем в строках с 33 по 43.

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

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

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

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

5.1. Статьи в серии

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

  • ссылка:/jmockit-101[JMockit 101]

  • ссылка:/jmockit-ожидания[Руководство по ожиданиям JMockit]

  • ссылка:/jmockit-advanced-use[Расширенные темы JMockit]