Как модульные тесты должны настраивать источники данных, когда они не работают на сервере приложений? - PullRequest
6 голосов
/ 07 мая 2009

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

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

У меня есть устаревшее приложение, работающее под 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.

Ответы [ 7 ]

5 голосов
/ 07 мая 2009

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

4 голосов
/ 07 мая 2009

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

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

2 голосов
/ 07 мая 2009

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

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

1 голос
/ 07 мая 2009
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 в конструктор, что-то нарушают. Затем, когда код становится лучше, более тестируемым, вы можете писать модульные тесты. (По определению, если тест касается файловой системы, сети или базы данных, это не модульный тест - это интеграционный тест.)

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

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

1 голос
/ 07 мая 2009

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

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

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

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

1 голос
/ 07 мая 2009

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

 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
  }
0 голосов
/ 07 мая 2009

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

...