Как мне имитировать классы, которые сложно создать (javax.mail.Message)?

#java #unit-testing #mocking #jakarta-mail

#java #модульное тестирование #издевательство #джакарта-почта

Вопрос:

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

Вот проблема: я хочу написать класс фильтра электронной почты, который выполняет итерацию по List<javax.mail.Message> и фильтрует сообщения электронной почты по теме, дате, откуда, кому и т.д.Код для тестирования выглядит следующим образом:

 public List<Message> doFilter(List<Message> messageList) {

    List<Message> newList = new ArrayList<Message>(messageList.size());

    try {
        for (Message message: messageList) {
            if (start != null) {
                Date sentDate = message.getSentDate();
                if (sentDate == null || sentDate.before(start))
                    continue;
            }
            if (end != null) {
                Date receivedDate = message.getReceivedDate();
                if (receivedDate == null || receivedDate.after(end))
                    continue;
            }
            newList.add(message);
        }
    }
    catch(Exception e) {
        e.printStackTrace();
    }
    return newList;
}
  

Итак, очевидный тестовый пример — создать List из нескольких сообщений и проверить, содержит ли новый список, возвращаемый фильтром, правильные сообщения.

Но javax.mail.Message является абстрактным и не может быть создан экземпляр напрямую. Для этого мне нужно было бы настроить реальное хранилище электронной почты, включая имя учетной записи и пароль.

Итак, мои вопросы:

  • Как мне имитировать несколько javax.mail.Message объектов с разными значениями, чтобы мой класс filter мог вызывать message.getSentDate() и другие Message методы, извлекая значения, которые я определил в коде настройки теста?

  • Какие из макетных пакетов лучше всего подходят для решения такого рода задач?

Все ответы действительно ценятся.

Ответ №1:

Я попробовал оба подхода, чтобы узнать о преимуществах и недостатках:

Подход 1: создайте новый класс MockMessage, который реализует только те методы, которые мне нужны в моем коде фильтра, затем напишите стандартные тесты JUnit.

Проблема с отсутствующим абстрактным методом на самом деле тривиальна, поскольку эти методы могут быть сгенерированы используемой мной IDE (они будут пустыми, но в любом случае они не нужны тестируемому коду). На самом деле никакой макет пакета не используется.

 public class MockMessage extends Message {

    private Date sentDate;

    public MockMessage(Date sentDate) {
        this.sentDate = sentDate;
    }

    public Date getSentDate() throws MessagingException {
        return sentDate;
    }

    // the rest of the required methods can easily be generated by the IDE
    ...
}


public void testFilter1() {

    try {
        DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ss");
        final Date date0 = dtf.parseDateTime("2011-05-19 05:51:26").toDate();
        final Date date1 = dtf.parseDateTime("2011-05-19 05:51:27").toDate();
        final Date date2 = dtf.parseDateTime("2011-05-19 05:51:28").toDate();

        final List<Message> mockMessages = Arrays.asList(
            (Message)new MockMessage(date0),
            (Message)new MockMessage(date1),
            (Message)new MockMessage(date2)
        );

        MessageFilter filter = new ByDate(date1, null);
        List<Message> result = filter.doFilter(mockMessages);
        assertEquals(result.size(),2);
        assertEquals(result.get(0).getSentDate(),date1);
        assertEquals(result.get(1).getSentDate(),date2);
    }
    catch(Exception e) {
        fail(e.getMessage());
    }
}
  

Подход 2: использование JMock и ClassImposteriser. Объем кода немного меньше, чем в подходе 1, и никакого дополнительного класса MockMessage не требуется.

Я все еще предпочитаю первый подход, поскольку он проще для понимания. Второй подход может быть полезен в других ситуациях, когда объекты создавать намного сложнее, чем javax.mail.Message .

 public void testFilter2() {

    try {
        DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd hh:mm:ss");
        final Date date0 = dtf.parseDateTime("2011-05-19 05:51:26").toDate();
        final Date date1 = dtf.parseDateTime("2011-05-19 05:51:27").toDate();
        final Date date2 = dtf.parseDateTime("2011-05-19 05:51:28").toDate();

        Mockery mockery = new Mockery();
        mockery.setImposteriser(ClassImposteriser.INSTANCE);

        final List<Message> mockMessages = Arrays.asList(
            mockery.mock(Message.class, "message 1"),
            mockery.mock(Message.class, "message 2"),
            mockery.mock(Message.class, "message 3")
        );

        mockery.checking(new Expectations() {{
           allowing(mockMessages.get(0)).getSentDate(); will(returnValue(date0));
           allowing(mockMessages.get(1)).getSentDate(); will(returnValue(date1));
           allowing(mockMessages.get(2)).getSentDate(); will(returnValue(date2));
        }});

        MessageFilter filter = new ByDate(date1, null);
        List<Message> result = filter.doFilter(mockMessages);
        assertEquals(result.size(),2);
        assertEquals(result.get(0).getSentDate(),date1);
        assertEquals(result.get(1).getSentDate(),date2);
    }
    catch(Exception e) {
        fail(e.getMessage());
    }
}
  

Ответ №2:

Просто создайте класс MockMessage, который расширяет Message. Затем вы можете реализовать или переопределить поведение, которое вы хотите имитировать.

Что касается макетных фреймворков, я обнаружил, что с ними больше проблем, чем они того стоят, но YMMV.

Комментарии:

1. Если вы не считаете, что насмешливые фреймворки того стоят, тогда вам нужно больше почитать о них. Они необходимы для правильного модульного тестирования.

2. javax.mail.Message имеет множество абстрактных методов, поэтому расширение Message выполнимо, но непрактично.

3. Я не понимаю, почему количество методов в интерфейсе должно влиять на ваше решение о его реализации (за исключением, может быть, того, чтобы дать вам понять, что это дерьмовый интерфейс, и его следует избегать). Любая приличная IDE в любом случае сгенерирует реализации для вас.

4. вы правы, в Jetbrains IDEA действительно есть команда для генерации отсутствующих методов, но я не знал об этом до сих пор.

Ответ №3:

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

Вот простой пример использования JMock (фреймворк, с которым я наиболее знаком):

 Mockery mockery = new Mockery();
mockery.setImposteriser(ClassImposteriser.INSTANCE);

final List<Message> mockMessages = asList(
    mockery.mock(Message.class, "message1"),
    mockery.mock(Message.class, "message2")
);

mockery.checking(new Expectations() {{
   allowing(mockMessages.get(0)).getSubject(); will(returnValue("some subject"));
   allowing(mockMessages.get(1)).getSubject(); will(returnValue("some other subject"));
}});
  

Обычно я бы отговаривал людей от издевательств над такого рода вещами и просто создавал объекты напрямую, но JavaMail API настолько неудобен в использовании, что понятно, что они не хотят этого делать.