«1. Введение

В прошлом мы много говорили о JMockit и Mockito.

В этом уроке мы познакомим вас с другим инструментом для имитации — EasyMock.

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

Прежде чем мы углубимся, давайте добавим следующую зависимость в наш pom.xml:

<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>3.5.1</version>
    <scope>test</scope>
</dependency>

Последнюю версию всегда можно найти здесь.

3. Основные понятия

При создании макета мы можем имитировать целевой объект, указать его поведение и, наконец, проверить, используется ли он так, как ожидалось.

Работа с макетами EasyMock включает четыре шага:

  1. creating a mock of the target class
  2. recording its expected behavior, including the action, result, exceptions, etc.
  3. using mocks in tests
  4. verifying if it’s behaving as expected

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

В конце концов, мы проверяем, все ли идет так, как ожидалось.

Четыре шага, упомянутые выше, относятся к методам в org.easymock.EasyMock:

  1. mock(…): generates a mock of the target class, be it a concrete class or an interface. Once created, a mock is in “recording” mode, meaning that EasyMock will record any action the Mock Object takes, and replay them in the “replay” mode
  2. expect(…): with this method, we can set expectations, including calls, results, and exceptions, for associated recording actions
  3. replay(…): switches a given mock to “replay” mode. Then, any action triggering previously recorded method calls will replay “recorded results”
  4. verify(…): verifies that all expectations were met and that no unexpected call was performed on a mock

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

4. Практический пример мока

Прежде чем мы продолжим, давайте взглянем на контекст примера: скажем, у нас есть читатель блога Baeldung, который любит просматривать статьи на веб-сайте, а затем он/она пытается писать статьи.

Начнем с создания следующей модели:

public class BaeldungReader {

    private ArticleReader articleReader;
    private IArticleWriter articleWriter;

    // constructors

    public BaeldungArticle readNext(){
        return articleReader.next();
    }

    public List<BaeldungArticle> readTopic(String topic){
        return articleReader.ofTopic(topic);
    }

    public String write(String title, String content){
        return articleWriter.write(title, content);
    }
}

В этой модели у нас есть два закрытых члена: articleReader (конкретный класс) и articleWriter (интерфейс).

Далее мы создадим их моки, чтобы проверить поведение BaeldungReader.

5. Мок с Java-кодом

Давайте начнем с создания макета для ArticleReader.

5.1. Типичная насмешка

Мы ожидаем, что метод articleReader.next() будет вызываться, когда читатель пропускает статью:

@Test
public void whenReadNext_thenNextArticleRead(){
    ArticleReader mockArticleReader = mock(ArticleReader.class);
    BaeldungReader baeldungReader
      = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();

    verify(mockArticleReader);
}

В приведенном выше примере кода мы строго придерживаемся четырехэтапной процедуры и имитируем класс ArticleReader. .

Хотя на самом деле нам все равно, что возвращает mockArticleReader.next(), нам все равно нужно указать возвращаемое значение для mockArticleReader.next() с помощью expect(…).andReturn(…).

С expect(…) EasyMock ожидает, что метод вернет значение или выдаст исключение.

Если мы просто сделаем:

mockArticleReader.next();
replay(mockArticleReader);

EasyMock будет жаловаться на это, поскольку он требует вызова expect(…).andReturn(…), если метод что-то возвращает.

Если это метод void, мы можем ожидать его действия с помощью expectLastCall() следующим образом:

mockArticleReader.someVoidMethod();
expectLastCall();
replay(mockArticleReader);

5.2. Порядок воспроизведения

Если нам нужно, чтобы действия воспроизводились в определенном порядке, мы можем быть более строгими:

@Test
public void whenReadNextAndSkimTopics_thenAllAllowed(){
    ArticleReader mockArticleReader
      = strictMock(ArticleReader.class);
    BaeldungReade baeldungReader
      = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleReader.ofTopic("easymock")).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();
    baeldungReader.readTopic("easymock");

    verify(mockArticleReader);
}

В этом фрагменте мы используем strictMock(…) для проверки порядка вызовов методов. Для макетов, созданных mock(…) и strictMock(…), любые неожиданные вызовы методов вызовут AssertionError.

Чтобы разрешить любой вызов метода для макета, мы можем использовать niceMock(…):

@Test
public void whenReadNextAndOthers_thenAllowed(){
    ArticleReader mockArticleReader = niceMock(ArticleReader.class);
    BaeldungReade baeldungReader = new BaeldungReader(mockArticleReader);

    expect(mockArticleReader.next()).andReturn(null);
    replay(mockArticleReader);

    baeldungReader.readNext();
    baeldungReader.readTopic("easymock");

    verify(mockArticleReader);
}

Здесь мы не ожидали вызова baeldungReader.readTopic(…), но EasyMock победил Не жалуйся. С niceMock(…) EasyMock теперь заботится только о том, выполнил ли целевой объект ожидаемое действие или нет.

5.3. Имитация Exception Throws

Теперь давайте продолжим имитацию интерфейса IArticleWriter и то, как обрабатывать ожидаемые Throwables:

@Test
public void whenWriteMaliciousContent_thenArgumentIllegal() {
    // mocking and initialization

    expect(mockArticleWriter
      .write("easymock","<body onload=alert('baeldung')>"))
      .andThrow(new IllegalArgumentException());
    replay(mockArticleWriter);

    // write malicious content and capture exception as expectedException

    verify(mockArticleWriter);
    assertEquals(
      IllegalArgumentException.class, 
      expectedException.getClass());
}

В приведенном выше фрагменте мы ожидаем, что articleWriter достаточно надежен, чтобы обнаруживать XSS (межсайтовый скриптинг) атаки.

Поэтому, когда читатель пытается внедрить вредоносный код в содержимое статьи, автор должен сгенерировать исключение IllegalArgumentException. Мы записали это ожидаемое поведение, используя expect(…).andThrow(…).

6. Макет с аннотацией

EasyMock также поддерживает внедрение макетов с использованием аннотаций. Чтобы использовать их, нам нужно запустить наши модульные тесты с помощью EasyMockRunner, чтобы он обрабатывал аннотации @Mock и @TestSubject.

Давайте перепишем предыдущие фрагменты:

@RunWith(EasyMockRunner.class)
public class BaeldungReaderAnnotatedTest {

    @Mock
    ArticleReader mockArticleReader;

    @TestSubject
    BaeldungReader baeldungReader = new BaeldungReader();

    @Test
    public void whenReadNext_thenNextArticleRead() {
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        baeldungReader.readNext();
        verify(mockArticleReader);
    }
}

Эквивалент mock(…), макет будет вставлен в поля, аннотированные с помощью @Mock. И эти макеты будут внедрены в поля класса, аннотированные @TestSubject.

В приведенном выше фрагменте мы явно не инициализировали поле articleReader в baeldungReader. При вызове baeldungReader.readNext() мы можем интерпретировать неявно вызываемый mockArticleReader.

Это произошло потому, что mockArticleReader был внедрен в поле articleReader.

«Обратите внимание, что если мы хотим использовать другое средство запуска тестов вместо EasyMockRunner, мы можем использовать тестовое правило JUnit EasyMockRule: нам приходится повторять вручную:

public class BaeldungReaderAnnotatedWithRuleTest {

    @Rule
    public EasyMockRule mockRule = new EasyMockRule(this);

    //...

    @Test
    public void whenReadNext_thenNextArticleRead(){
        expect(mockArticleReader.next()).andReturn(null);
        replay(mockArticleReader);
        baeldungReader.readNext();
        verify(mockArticleReader);
    }

}

Это некрасиво, и нам нужно элегантное решение.

К счастью, у нас есть класс EasyMockSupport в EasyMock, который поможет справиться с этим. Это помогает отслеживать моки, чтобы мы могли воспроизвести и проверить их в пакете следующим образом:

replay(A);
replay(B);
replay(C);
//...
verify(A);
verify(B);
verify(C);

Здесь мы имитировали как articleReader, так и articleWriter. При установке этих макетов в режим «воспроизведения» мы использовали статический метод replayAll(), предоставленный EasyMockSupport, и использовали verifyAll() для проверки их поведения в пакетном режиме.

Мы также ввели метод times(…) на этапе ожидания. Это помогает указать, сколько раз мы ожидаем, что метод будет вызываться, чтобы мы могли избежать дублирования кода.

//...
public class BaeldungReaderMockSupportTest extends EasyMockSupport{

    //...

    @Test
    public void whenReadAndWriteSequencially_thenWorks(){
        expect(mockArticleReader.next()).andReturn(null)
          .times(2).andThrow(new NoSuchElementException());
        expect(mockArticleWriter.write("title", "content"))
          .andReturn("BAEL-201801");
        replayAll();

        // execute read and write operations consecutively
 
        verifyAll();
 
        assertEquals(
          NoSuchElementException.class, 
          expectedException.getClass());
        assertEquals("BAEL-201801", articleId);
    }

}

Мы также можем использовать EasyMockSupport через делегирование:

Раньше мы использовали статические методы или аннотации для создания макетов и управления ими. Под капотом эти статические и аннотированные макеты контролируются глобальным экземпляром EasyMockSupport.

Здесь мы явно создали его экземпляр и взяли все эти моки под свой собственный контроль через делегирование. Это может помочь избежать путаницы, если в нашем тестовом коде с EasyMock возникнут конфликты имен или возникнут какие-либо подобные случаи.

EasyMockSupport easyMockSupport = new EasyMockSupport();

@Test
public void whenReadAndWriteSequencially_thenWorks(){
    ArticleReader mockArticleReader = easyMockSupport
      .createMock(ArticleReader.class);
    IArticleWriter mockArticleWriter = easyMockSupport
      .createMock(IArticleWriter.class);
    BaeldungReader baeldungReader = new BaeldungReader(
      mockArticleReader, mockArticleWriter);

    expect(mockArticleReader.next()).andReturn(null);
    expect(mockArticleWriter.write("title", "content"))
      .andReturn("");
    easyMockSupport.replayAll();

    baeldungReader.readNext();
    baeldungReader.write("title", "content");

    easyMockSupport.verifyAll();
}

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

В этой статье мы кратко рассказали об основах использования EasyMock, о том, как генерировать фиктивные объекты, записывать и воспроизводить их поведение, а также проверять правильность их поведения.

Если вам интересно, ознакомьтесь с этой статьей для сравнения EasyMock, Mocket и JMockit.

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

«

As always, the full implementation can be found over on Github.