Как модульные тесты должны настраивать источники данных, когда они не работают на сервере приложений?

StackOverflow https://stackoverflow.com/questions/831760

Вопрос

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

Позвольте мне сделать шаг назад и задать вопрос, мотивирующий мой оригинальный вопрос.

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

Унаследованный код получает источник данных следующим образом:

(jndiName - определенная строка)

Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

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

Итак, мой реальный вопрос: как правильно это сделать? Есть ли какой-то одобренный способ, которым модульный тест может настроить контекст так, чтобы он возвращал соответствующий источник данных, чтобы тестируемый код не должен был знать, где он работает?

<Ч>

Для контекста: МОЙ ОРИГИНАЛЬНЫЙ ВОПРОС:

У меня есть некоторый Java-код, который должен знать, работает ли он под JBoss. Есть ли канонический способ для кода определить, выполняется ли он в контейнере?

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

private boolean isRunningUnderJBoss(Context ctx) {
        boolean runningUnderJBoss = false;
        try {
            // The following invokes a naming exception when not running under
            // JBoss.
            ctx.getNameInNamespace();

            // The URL packages must contain the string "jboss".
            String urlPackages = (String) ctx.lookup("java.naming.factory.url.pkgs");
            if ((urlPackages != null) && (urlPackages.toUpperCase().contains("JBOSS"))) {
                runningUnderJBoss = true;
            }
        } catch (Exception e) {
            // If we get there, we are not under JBoss
            runningUnderJBoss = false;
        }
        return runningUnderJBoss;
    }

Context ctx = new InitialContext();
if (isRunningUnderJboss(ctx)
{
.........

Теперь, похоже, это работает, но похоже на взлом. Что такое «правильно» способ сделать это? В идеале мне бы хотелось, чтобы этот способ работал с различными серверами приложений, а не только с JBoss.

Это было полезно?

Решение

Весь подход кажется мне неправильным. Если вашему приложению нужно знать, в каком контейнере оно работает, вы делаете что-то не так.

Когда я использую Spring, я могу перейти от Tomcat к WebLogic и обратно, ничего не меняя. Я уверен, что при правильной настройке я мог бы сделать то же самое с JBOSS. Это цель, за которую я бы стрелял.

Другие советы

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

Некоторая комбинация внедрения зависимостей (будь то через Spring, файлы конфигурации или аргументы программы) и заводской шаблон обычно работают лучше всего.

В качестве примера я передаю аргумент в мои скрипты Ant, которые устанавливают файлы конфигурации в зависимости от того, идет ли речь о войне или войне в среду разработки, тестирования или производства.

Возможно, что-то вроде этого (безобразно, но это может работать)

 private void isRunningOn( String thatServerName ) { 

     String uniqueClassName = getSpecialClassNameFor( thatServerName );
     try { 
         Class.forName( uniqueClassName );
     } catch ( ClassNotFoudException cnfe ) { 
         return false;
     }
     return true;
 } 

Метод getSpecialClassNameFor возвращает класс, уникальный для каждого сервера приложений (и может возвращать имена новых классов при добавлении большего количества серверов приложений)

Тогда вы используете это как:

  if( isRunningOn("JBoss")) {
         createJBossStrategy....etcetc
  }
Context ctx = new InitialContext();
DataSource dataSource = (DataSource) ctx.lookup(jndiName);

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

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

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

public class Foo {
  private final DataSource dataSource;
  public Foo() { // production code calls this - no changes needed to callers
    Context ctx = new InitialContext();
    this.dataSource = (DataSource) ctx.lookup(jndiName);
  }
  public Foo(DataSource dataSource) { // test code calls this
    this.dataSource = dataSource;
  }
  // methods that use dataSource
}

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

Преимущество модульных тестов состоит в том, что они работают быстро - сотни или тысячи в секунду - и очень сосредоточены на тестировании только одного поведения за раз. Это позволяет запускать их часто (если вы не решаетесь запустить все модульные тесты после замены одной строки, они запускаются слишком медленно), чтобы вы могли быстро получить обратную связь. И поскольку они очень сфокусированы, вы просто узнаете по названию провалившегося теста, где именно в рабочем коде ошибка.

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

Есть несколько способов решения этой проблемы. Один из них - передать объект Context в класс, когда он проходит модульное тестирование. Если вы не можете изменить сигнатуру метода, реорганизуйте создание начального контекста в защищенный метод и протестируйте подкласс, который возвращает макетированный объект контекста, переопределив метод. Это может, по крайней мере, поставить класс под тест, чтобы вы могли рефакторинг найти лучшие альтернативы оттуда.

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

Одна вещь, о которой стоит подумать: когда у вас будет это соединение с базой данных из контейнера, что вы собираетесь с ним делать? Это проще, но это не совсем модульный тест, если вам нужен целый уровень доступа к данным.

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

Простой способ сделать это - настроить прослушиватели жизненного цикла в web.xml . Они могут установить глобальные флаги, если хотите. Например, вы можете определить ServletContextListener в вашем web.xml и в методе contextInitialized установите глобальный флаг, который вы запускаете внутри контейнера. Если глобальный флаг не установлен, значит, вы не работаете внутри контейнера.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top