В погоне за качеством кода: Тестирование модулей в приложениях Ajax

GWT облегчает тестирование асинхронных приложений

Возможно, вы получаете наслаждение от написания приложений на Ajax, но процесс их тестирования определенно мучителен. В этой статье Эндрю Гловер рассматривает негативную сторону Ajax (вернее, одну из них) – а именно, врожденную сложность функционального тестирования асинхронных Web-приложений. К счастью, этого дракона не так уж сложно укротить - с помощью инструментария Google Web Toolkit.

Эндрю Гловер, президент компании, Stelligent Incorporated

Эндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.



19.11.2007

Ajax, несомненно, одна из наиболее популярных тем в среде Web-разработчиков за последнее время - распространение инструментов, сред, книг и Web-сайтов свидетельствует о том, что эта технология останется надолго. И, кроме всего прочего, приложения Ajax очень хороши, не так ли? С другой стороны, каждый, что разрабатывал приложения на Ajax, может подтвердить, что тестировать Ajax не так просто. На самом деле появление Ajax фактически сделало несостоятельными множество инструментов и сред тестирования, которые не были предназначены для тестирования асинхронных Web-приложений!

Примечательно, что разработчики одной из сред, поддерживающих Ajax, обратили внимание на это ограничение и сделали нечто действительно новое: они встроили возможность тестирования в саму среду. Более того, поскольку эта среда упрощает создание приложений Ajax с использованием кода Java™, а не JavaScript, она, если можно так выразиться, опирается на плечи гигантов и пользуется возможностями практически стандартной среды тестирования для платформы Java: JUnit.

Среда, о которой я говорю - это, конечно же, чрезвычайно популярный инструментарий Google Web Toolkit, также известный как GWT. В этой статье я покажу вам, как совместимость GWT с Java позволяет создавать приложения Ajax с такими же возможностями тестирования, как у их синхронных товарищей.

Повышайте качество кода

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

JUnit и GWTTestCase

Поскольку приложения Ajax, созданные с помощью GWT, написаны на Java, они отлично подходят для тестирования разработчиками в JUnit. Более того, команда разработчиков GWT создала класс helper GWTTestCase, который расширяет соответствующий класс JUnit 3.8.1 TestCase. Этот базовый класс добавляет возможности для тестирования кода GWT, а также некоторые вспомогательные возможности для запуска компонентов GWT.

Инструментарий Google Web Toolkit

Инструментарий Google Web Toolkit был выпущен для сообщества Web-разработчиков Java с большой помпой и был принят с равным по силе энтузиазмом. GWT предоставляет инновационный способ использования кода Java для проектирования, создания и развертывания Web-приложений с поддержкой Ajax. Вместо того чтобы изучать JavaScript и тратить часы на распутывание проблем конкретных браузеров, разработчик Web-приложений на Java может сразу приступить к разработке динамических Web-приложений с богатой функциональностью, типичной для Ajax.

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

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

Рисунок 1. Простая форма с возможностями Ajax
Простая форма с возможностями Ajax.

Когда вы вводите допустимое слово и нажимаете кнопку Submit в этом компоненте, серверу отправляется сообщение с запросом определения слова. Определение возвращается асинхронно через функцию обратного вызова и вставляется в нужное место Web-страницы, как показано на рисунке 2:

Рисунок 2. После нажатия на кнопку Submit выводится ответ
После нажатия на кнопку Submit выводится ответ.

Функциональное и интеграционное тестирование

Тестирование взаимодействия, показанного на рисунке 2, может быть разбито на несколько различных сценариев, но особенно выделяются два из них. С функциональной точки зрения вам, вероятно, захочется написать тест, который будет заполнять форму, нажимать на кнопку Submit и после этого проверять наличие определения под формой. Другой вариант - это интеграционное тестирование, которое позволит вам проверить работу асинхронного механизма клиентского кода. Компонент GWTTestCase инструментария GWT был разработан именно для такого тестирования.

Важно помнить, что в среде тестирования GWTTestCase нельзя выполнить тестирование взаимодействия с пользователем. При проектировании и разработке приложений GWT необходимо думать о тестировании кода, не полагаясь на взаимодействие с пользователем. Это значит, что необходимо отделить код взаимодействия от бизнес-логики, что, как вы уже знаете, и само по себе представляет собой правильный подход.

Например, посмотрим еще раз на приложение Ajax, показанное на рисунках 1 и 2. Это приложение состоит из четырех логических компонентов: текстового поля TextBox для ввода слова, кнопки Button для нажатия и двух меток Label (одна для TextBox, а вторая для того места, где отображается определение). Обычный подход к созданию модуля GWT может выглядеть так, как вы можете видеть в листинге 1, однако как тестировать этот код?

Листинг 1. Работающее приложение GWT, но как можно его проверить?
public class DefaultModule implements EntryPoint {

 public void onModuleLoad() {
   Button button = new Button("Submit");
   TextBox box = new TextBox();
   Label output = new Label();
   Label label = new Label("Word: ");

   HorizontalPanel inputPanel = new HorizontalPanel();
   inputPanel.setStyleName("input-panel");
   inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
   inputPanel.add(label);
   inputPanel.add(box);

   button.addClickListener(new ClickListener() {
    public void onClick(Widget sender) {
      String word = box.getText();
      WordServiceAsync instance = WordService.Util.getInstance();
       try {
         instance.getDefinition(word, new AsyncCallback() {
           
           public void onFailure(Throwable error) {
             Window.alert("Error occurred:" + error.toString());
           }

           public void onSuccess(Object retValue) {
             output.setText(retValue.toString());
           }
		  });
        }catch(Exception e) {
          e.printStackTrace();
		}
	}
   });

   inputPanel.add(button);
   inputPanel.setCellVerticalAlignment(button,
     HasVerticalAlignment.ALIGN_BOTTOM);

   RootPanel.get("slot1").add(inputPanel);
   RootPanel.get("slot2").add(output);
   }
}

Несмотря на то, что код в листинге 1 работает, у него есть один большой недостаток: он не может быть протестирован так, как это делается вGWTTestCase JUnit и GWT. На самом деле, если я попытаюсь написать тесты для этого кода, технически они будут запускаться, но логически работать не будут. Теперь задумайтесь: как бы вы проверили этот код? Единственный public-метод, доступный для тестирования, возвращает void. Как проверить, правильно ли он работает?

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


Отделение кода интерфейса от бизнес-логики

Первый шаг, показанный в листинге 2, состоит в создании методов доступа (accessor) для каждого компонента интерфейса. Таким образом, если мне понадобится, я могу перехватить их.

Листинг 2. Добавление методов доступа для компонентов интерфейса
public class WordModule implements EntryPoint {

  private Label label;
  private Button button;
  private TextBox textBox;
  private Label outputLabel;
  
  protected Button getButton() {
   if (this.button == null) {
    this.button = new Button("Submit");
   }
   return this.button;
  }

  protected Label getLabel() {
   if (this.label == null) {
    this.label = new Label("Word: ");
   }
   return this.label;
  }

  protected Label getOutputLabel() {
   if (this.outputLabel == null) {
    this.outputLabel = new Label();
   }
   return this.outputLabel;
  }

  protected TextBox getTextBox() {
   if (this.textBox == null) {
    this.textBox = new TextBox();
    this.textBox.setVisibleLength(20);
   }
   return this.textBox;
  }
}

Теперь я реализовал программный доступ ко всем компонентам интерфейса (предполагая, что все классы, к которым нужен доступ, находятся в том же пакете). Только время покажет, понадобится ли мне хотя бы один из этих методов для проверки. Я надеюсь ограничить использование этих методов, поскольку, как я уже говорил, GWT не предназначен для тестирования взаимодействий. В этом случае я пытаюсь проверить не то, действительно ли была нажата кнопка, а то, вызвал ли модуль GWT мой серверный код для заданного слова и вернул ли сервер нужное определение. Я делаю это, преобразовывая логику получения определения метода onModuleLoad() в метод, поддающийся тестированию, который показан в листинге 3:

Листинг 3. Измененный метод onModuleLoad становится более приспособленным к тестированию
public void onModuleLoad() {
  HorizontalPanel inputPanel = new HorizontalPanel();
  inputPanel.setStyleName("disco-input-panel");
  inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);

  Label lbl = this.getLabel();
  inputPanel.add(lbl);

  TextBox txBox = this.getTextBox();
  inputPanel.add(txBox);

  Button btn = this.getButton();

  btn.addClickListener(new ClickListener() {
   public void onClick(Widget sender) {
     submitWord();
   }
  });

  inputPanel.add(btn);
  inputPanel.setCellVerticalAlignment(btn,
      HasVerticalAlignment.ALIGN_BOTTOM);

  if(RootPanel.get("input-container") != null) {
   RootPanel.get("input-container").add(inputPanel);
  }

  Label output = this.getOutputLabel();
  if(RootPanel.get("output-container") != null) {
   RootPanel.get("output-container").add(output);
  }
}

Как можно видеть из листинга 3, я фактически перенес логику получения определения onModuleLoad() в метод submitWord, определенный ниже в листинге 4:

Листинг 4. Основное содержание Ajax-приложения
protected void submitWord() {
  String word = this.getTextBox().getText().trim();
  this.getDefinition(word);
}

protected void getDefinition(String word) {
 WordServiceAsync instance = WordService.Util.getInstance();
 try {
   instance.getDefinition(word, new AsyncCallback() {

   public void onFailure(Throwable error) {
    Window.alert("Error occurred:" + error.toString());
   }

   public void onSuccess(Object retValue) {
    getOutputLabel().setText(retValue.toString());
   }
  });
 }catch(Exception e) {
   e.printStackTrace();
 }
}

Содержимое метода submitWord() переносится в метод getDefinition(), который я могу проверить с помощью JUnit. Метод getDefinition() логически отделен от кода, связанного с интерфейсом (большей частью) и может быть вызван без привязки к нажатию на кнопку. С другой стороны, проблемы состояния, свойственные асинхронным приложениям, и семантические правила языка Java не дают мне полностью Если внимательно рассмотреть код, приведенный в листинге 4, можно увидеть, что метод getDefinition(), который обращается к функции обратного вызова, работает с некоторыми компонентами интерфейса, а именно с окном сообщений для вывода ошибок и экземпляром Label.

Я все-таки могу проверить работу приложения, получив указатель на вывод экземпляра Label и предполагая, что его текст является определением заданного слова. При тестировании с помощью GWTTestCase, лучше не пытаться вручную изменить состояние компонента, предоставив это GWT. Например, в листинге 4 я хочу проверить, что определение заданного слова возвращается и размещается в Label для вывода. Мне не нужно реально управлять компонентами интерфейса, чтобы задать слово; я могу просто напрямую вызывать метод getDefinition и предполагать, что в Label установлено соответствующее определение.

Теперь, когда я переписал приложение GWT с учетом тестирования, мне нужно написать для него тест, то есть настроить модуль GWT GWTTestCase.


Настройка GWTTestCase

GWTTestCase обязывает вас играть по определенным правилам, если вы желаете воспользоваться его возможностями. К счастью, эти правила просты:

  • Все классы реализации теста должны располагаться в том же пакете, что и модуль GWT, который вы пытаетесь проверить.
  • При запуске тестов вы должны передать как минимум один аргумент VM, чтобы указать, в каком режиме GWT будет проведен тест (локально или через Интернет).
  • Необходимо реализовать метод getModuleName(), который будет возвращать представление String файла модуля XML.

И последнее, поскольку приложение Ajax, связывающееся с сервером, асинхронно по своей природе, GWT предлагает дополнительный класс Timer, который помогает задержать JUnit на время, достаточное для завершения асинхронных запросов до выполнения соответствующего предположения.

Реализация класса Timer и getModuleName

Как я уже упоминал выше, основные усилия по тестированию будут сконцентрированы на методе getDefinition() (показан в листинге 4). Как можно видеть из кода, логика теста весьма проста: передать слово (например, pugnacious) и проверить, что в тексте соответствующей Label содержится верное определение. Просто, не так ли? Однако не стоит забывать, что в методе getDefinition() скрыта некоторая асинхронность, появляющаяся за счет метода AsyncCallback.

Класс GWTTestCase является абстрактным, поскольку таким образом определен его метод getModuleName(); следовательно, когда вы расширяете класс, вам необходимо реализовать getModuleName() (за исключением ситуаций, когда вы создаете собственный базовый абстрактный класс для среды). Название модуля, по существу, отражает структуру пакета, где хранятся XML-файлы GWT, за исключением расширений файлов. Например, в моем случае файл XML называется WordModule.gwt.xml и хранится в следующей структуре каталогов: com/acme/gwt. Следовательно, логическим названием модуля является com.acme.gwt.WordModule, что должно напомнить вам об обычной схеме формирования названий пакетов на платформе Java.

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

Листинг 5. Необходимо реализовать метод getModuleName и указать верное имя
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;

public class WordModuleTest extends GWTTestCase {

 public String getModuleName() {
   return "com.acme.gwt.WordModule";
 }
}

Пока все хорошо, но я еще ничего не проверил! Поскольку в моем приложении Ajax используется объект AsyncCallback, мне нужно задержать завершение работы JUnit при вызове метода getDefinition() через тестовый пример; в противном случае тест завершится с ошибкой из-за отсутствия ответа. Здесь на помощь приходит класс GWT Timer. Timer позволяет заменить метод run класса getDefinition() и закончить логику теста в Timer. (Тестовый пример будет работать в отдельном потоке, фактически не давая JUnit завершить тестовый пример.)

Например, в моем тесте я сначала вызываю метод getDefinition() и после этого предоставляю реализацию метода run() класса Timer. Метод run() получает текст экземпляра Label для вывода и проверяет наличие в нем правильного определения. После того как я определил экземпляр Timer, мне нужно запланировать его запуск, также указав JUnit на необходимость задержаться в ожидании экземпляра Timer. Звучит достаточно сложно, но не пугайтесь, на практике все довольно просто. Фактически вы можете видеть весь сценарий в листинге 6:

Листинг 6. Тестирование с помощью GWT -- верное дело
public void testDefinitionValue() throws Exception {
 WordModule module = new WordModule();
 module.getDefinition("pugnacious");
 Timer timer = new Timer() {
   public void run() {
    String value = module.getOutputLabel().getText();
	String control = "inclined to quarrel or fight readily;...";   
	assertEquals("should be " + control, control, value);
    finishTest();
   }
  };
  timer.schedule(200);
  delayTestFinish(500);
}

Как вы видите, метод run() класса Timer, в котором я фактически проверяю функционирование моего приложения Ajax и использование им удаленного вызова процедуры. Обратите внимание, что последним действием в методе я вызываю метод finishTest(), который сигнализирует о том, что все прошло так, как и ожидалось, и JUnit может работать дальше обычным образом без блокировки. На практике вам может потребоваться скорректировать время задержки в зависимости от того, как долго будет выполняться асинхронная часть. Однако особенность тестирования приложений GWT с помощью JUnit состоит в том, что вы можете сделать это без установки полнофункционального Web-приложения. Это позволяет тестировать приложения GWT на более ранних этапах и, при желании, чаще.


Запуск теста GWT

Функциональное тестирование в GWT

Простое приложение Ajax, подобное показанному здесь, можно было бы проверить с функциональной точки зрения с помощью таких сред, как Selenium, которые заставляют браузер моделировать поведение реального пользователя. Однако чтобы запустить функциональный тест с помощью Selenium, у вас должно быть установлено полностью работающее Web-приложение.

Как я указывал выше, если вы захотите выполнять тесты GWT для JUnit, вам нужно будет сделать множество мелких настроек рабочей среды. Например, чтобы запустить тесты через задачу junit Ant, мне нужно сделать так, чтобы в пути классов присутствовали определенные файлы, и предоставить аргумент для JVM. В частности, когда я запускаю задачу junit, мне нужно убедиться в том, что директория (или директории), в которых содержался файлы исходного кода (в том числе тесты), находятся в пути классов, а также сообщить GWT, в каком режиме работать. Я обычно использую локальный (hosted) режим, то есть устанавливаю флаг www-test, показанный в листинге 7:

Листинг 7. Запуск теста GWT с помощью Ant
<junit dir="./" failureproperty="test.failure" printSummary="yes" 
	 fork="true" haltonerror="true">
	
 <jvmarg value="-Dgwt.args=-out www-test" />
 
 <sysproperty key="basedir" value="." />
 <formatter type="xml" />
 <formatter usefile="false" type="plain" />
 <classpath>
  <path refid="gwt-classpath" />
  <pathelement path="build/classes" />
  <pathelement path="src" />
  <pathelement path="test" />
 </classpath>
 <batchtest todir="${testreportdir}">
  <fileset dir="test">
   <include name="**/**Test.java" />
  </fileset>
 </batchtest>
</junit>

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


Заключение

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

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

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=270099
ArticleTitle=В погоне за качеством кода: Тестирование модулей в приложениях Ajax
publish-date=11192007