Как выполнить рефакторинг и модульное тестирование сложных устаревших методов Java EE5 EJB?

Мои коллеги и я в настоящее время внедряем модульные тесты в нашу устаревшую кодовую базу Java EE5. Мы используем в основном JUnit и Mockito. В процессе написания тестов мы заметили, что несколько методов в наших EJB трудно тестировать, потому что они делают много вещей одновременно.

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

Это пример одного из наших методов и его логических шагов в сервисе, управляющем очередью сообщений:

  • потреблятьСообщения

    • подтвердить ранее загруженные сообщения

    • getNewUnreadMessages

    • addExtraMessages (в зависимости от несколько сложных условий)

    • пометить сообщения как загруженные

    • сериализоватьмессажеобжектс

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

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

И вот я здесь.

Как вы совмещаете раскрытие легко тестируемых низкоуровневых методов с избеганием загромождения интерфейсов? В нашем случае интерфейсы EJB.

Я читал в других вопросах модульного теста, что следует использовать внедрение зависимостей или следовать принципу единой ответственности, но у меня возникли проблемы с его применением на практике. Кто-нибудь может подсказать, как применить такой шаблон к методу примера выше?

Не могли бы вы порекомендовать другие общие шаблоны объектно-ориентированного программирования или шаблоны Java EE?


person Alpha Hydrae    schedule 15.10.2012    source источник


Ответы (3)


На первый взгляд, я бы сказал, что нам, вероятно, нужно ввести новый класс, который бы 1) предоставлял общедоступные методы, которые можно тестировать, но 2) не отображались бы в общедоступном интерфейсе вашего API.

В качестве примера давайте представим, что вы разрабатываете API для автомобиля. Для реализации API вам понадобится движок (со сложным поведением). Вы хотите полностью протестировать свой двигатель, но не хотите раскрывать детали клиентам автомобильного API (все, что я знаю о своей машине, это как нажать кнопку запуска и как переключить радиоканал).

В этом случае я бы сделал что-то вроде этого:

public class Engine {
  public void doActionOnEngine() {}
  public void doOtherActionOnEngine() {}
}


public class Car {
  private Engine engine;

  // the setter is used for dependency injection
  public void setEngine(Engine engine) {
    this.engine = engine;
  }

  // notice that there is no getter for engine

  public void doActionOnCar() {
    engine.doActionOnEngine();
  }

  public void doOtherActionOnCar() {
    engine.doActionOnEngine();
    engine.doOtherActionOnEngine(),
  }
}

Для людей, использующих Car API, нет прямого доступа к движку, поэтому нет риска причинить вред. С другой стороны, можно полностью протестировать двигатель.

person Olivier Liechti    schedule 16.10.2012

Внедрение зависимостей (DI) и принцип единой ответственности (SRP) тесно связаны между собой.

SRP в основном утверждает, что каждый класс должен делать только одну вещь, а все остальные делегировать отдельным классам. Например, ваш метод serializeMessageObjects должен быть выделен в отдельный класс — назовем его MessageObjectSerializer.

DI означает внедрение (передачу) объекта MessageObjectSerializer в качестве аргумента вашему объекту MessageQueue — либо в конструкторе, либо при вызове метода consumeMessages. Для этого вы можете использовать DI-фреймворки, но я рекомендую делать это вручную, чтобы понять концепцию.

Теперь, если вы создадите интерфейс для MessageObjectSerializer, вы можете передать его MessageQueue, а затем вы получите полное значение шаблона, так как вы можете создавать макеты/заглушки для легкого тестирования. Внезапно consumeMessages перестал обращать внимание на то, как ведет себя serializeMessageObjects.

Ниже я попытался проиллюстрировать закономерность. Обратите внимание, что если вы хотите протестировать потребление сообщений, вам не нужно использовать объект MessageObjectSerializer. Вы можете сделать макет или заглушку, которые делают именно то, что вы хотите, и передать их вместо конкретного класса. Это действительно делает тестирование намного проще. Пожалуйста, простите синтаксические ошибки. У меня не было доступа к Visual Studio, поэтому написано в текстовом редакторе.

// THE MAIN CLASS
public class MyMessageQueue() 
{

    IMessageObjectSerializer _serializer; 



    //Constructor that takes the gets the serialization logic injected
    public MyMessageQueue(IMessageObjectSerializer serializer)
    {
        _serializer = serializer;

        //Also a lot of other injection 
    }


    //Your main method. Now it calls an external object to serialize
    public void consumeMessages()
    {
        //Do all the other stuff

        _serializer.serializeMessageObjects()
    }
}

//THE SERIALIZER CLASS
Public class MessageObjectSerializer : IMessageObjectSerializer 
{
    public List<MessageObject> serializeMessageObjects()
    {
        //DO THE SERILIZATION LOGIC HERE 
    }
}


//THE INTERFACE FOR THE SERIALIZER 
Public interface MessageObjectSerializer 
{
    List<MessageObject> serializeMessageObjects(); 
}

EDIT: Извините, мой пример на C#. Я надеюсь, что вы можете использовать его в любом случае :-)

person Morten    schedule 15.10.2012
comment
Спасибо за пример. В любом случае, он достаточно близок к Java, чтобы его почти невозможно было отличить! Я посмотрю, как я могу извлечь свои задачи в такой объект. - person Alpha Hydrae; 16.10.2012

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

Обычно программа настроена на использование определенных ресурсов, таких как определенный файл, IP-адрес, имя хоста и т. д. Чтобы противостоять этому, вам необходимо реорганизовать программу для использования внедрения зависимостей. Обычно это делается путем добавления параметров в конструктор, которые заменяют значения, закодированные в ahrdcode.

Также очень сложно тестировать большие классы и методы. Обычно это происходит из-за комбинаторного взрыва количества тестов, необходимых для проверки сложной логики. Чтобы противостоять этому, вы обычно сначала проводите рефакторинг, чтобы получить гораздо больше (но более коротких) методов, а затем пытаетесь сделать код более общим и тестируемым, извлекая несколько классов из исходного класса, каждый из которых имеет один метод входа (public) и несколько утилит. методы (частные). По сути, это принцип единой ответственности.

Теперь вы можете начать свой путь «вверх», тестируя новые классы. Это будет намного проще, так как с комбинаторикой гораздо проще работать на этом этапе.

В какой-то момент вы, вероятно, обнаружите, что можете значительно упростить свой код, используя следующие шаблоны проектирования: Command, Composite, Adaptor, Factory, Builder и Facade. Это наиболее распространенные шаблоны, которые сокращают беспорядок.

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

person Anders Johansen    schedule 15.10.2012