Статьи

TestNG и Selenium

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

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

Одна из причин, по которой мне нравится TestNG, заключается в том, что он плавно охватывает юнит-тесты и интеграционные тесты. Все внутренние тесты Tapestry (около 1500 отдельных тестов) написаны с использованием TestNG, а Tapestry включает базовый класс тестовых примеров для работы с Selenium : AbstractIntegrationTestSuite . Этот класс делает несколько полезных вещей:

  • Запускает ваше приложение с помощью Jetty
  • Запускает SeleniumServer (который управляет веб-браузером, который может использовать ваше приложение)
  • Создает экземпляр клиента Selenium
  • Реализует все методы Selenium , перенаправляя каждый на экземпляр Selenium
  • Добавляет дополнительные отчеты об ошибках при любых неудачных вызовах клиента Selenium

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

  • Он запускает ваше приложение с помощью Jetty 5 (в комплекте с SeleniumServer)
  • Он запускает и останавливает стек (Selenium, SeleniumServer, Jetty) вокруг каждого класса

Для моего текущего клиента пара ресурсов требует JNDI, и поэтому я использую Jetty 7 для запуска приложения (по крайней мере, в разработке, а возможно и в развертывании). К счастью, Jetty 5 использует старые пакеты org.mortbay.jetty, а Jetty 7 использует новые пакеты org.eclipse.jetty, поэтому обе версии сервера могут сосуществовать в одном приложении.

Большая проблема в том, что я не хотел ни одного титанического теста для всего моего приложения; Сначала я хотел разбить его по-другому, на странице «Гобелен».

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

Я немного рефакторинг, используя некоторые возможности TestNG, которые я ранее не использовал.

Часть AbstractIntegrationTestSuite, отвечающая за запуск и остановку стека , разбита на собственный класс. Этот новый класс, SeleniumLauncher, отвечает за запуск и остановку стека вокруг всего теста TestNG . В терминологии TestNG набор содержит несколько тестов , а тест содержит тестовые случаи (найденные в отдельных классах в отсканированных пакетах). Контрольный пример содержит методы тестирования и настройки.

Вот что я придумала:

package com.myclient.itest;

import org.apache.tapestry5.test.ErrorReportingCommandProcessor;
import org.eclipse.jetty.server.Server;
import org.openqa.selenium.server.RemoteControlConfiguration;
import org.openqa.selenium.server.SeleniumServer;
import org.testng.ITestContext;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;

import com.myclient.RunJetty;
import com.thoughtworks.selenium.CommandProcessor;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.HttpCommandProcessor;
import com.thoughtworks.selenium.Selenium;

public class SeleniumLauncher {

  public static final String SELENIUM_KEY = "myclient.selenium";

  public static final String BASE_URL_KEY = "myclient.base-url";

  public static final int JETTY_PORT = 9999;

  public static final String BROWSER_COMMAND = "*firefox";

  private Selenium selenium;

  private Server jettyServer;

  private SeleniumServer seleniumServer;

  /** Starts the SeleniumServer, the application, and the Selenium instance. */
  @BeforeTest(alwaysRun = true)
  public void setup(ITestContext context) throws Exception {

    jettyServer = RunJetty.start(JETTY_PORT);

    seleniumServer = new SeleniumServer();

    seleniumServer.start();

    String baseURL = String.format("http://localhost:%d/", JETTY_PORT);

    CommandProcessor cp = new HttpCommandProcessor("localhost",
        RemoteControlConfiguration.DEFAULT_PORT, BROWSER_COMMAND,
        baseURL);

    selenium = new DefaultSelenium(new ErrorReportingCommandProcessor(cp));

    selenium.start();

    context.setAttribute(SELENIUM_KEY, selenium);
    context.setAttribute(BASE_URL_KEY, baseURL);
  }

  /** Shuts everything down. */
  @AfterTest(alwaysRun = true)
  public void cleanup() throws Exception {
    if (selenium != null) {
      selenium.stop();
      selenium = null;
    }

    if (seleniumServer != null) {
      seleniumServer.stop();
      seleniumServer = null;
    }

    if (jettyServer != null) {
      jettyServer.stop();
      jettyServer = null;
    }
  }
}

Обратите внимание, что мы используем аннотации @BeforeTest и @AfterTest; это означает, что любое количество тестов может выполняться с использованием одного и того же стека. Стек запускается только один раз.

Также обратите внимание на то, как мы используем ITestContext для передачи информации тестам в форме атрибутов. TestNG имеет встроенную форму внедрения зависимостей; любой метод, которому нужен ITestContext, может получить его, просто объявив параметр этого типа.

AbstractIntegrationTestSuite2 — это новый базовый класс для написания интеграционных тестов:

package com.myclient.itest;

import java.lang.reflect.Method;

import org.apache.tapestry5.test.AbstractIntegrationTestSuite;
import org.apache.tapestry5.test.RandomDataSource;
import org.testng.Assert;
import org.testng.ITestContext;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

import com.mchange.util.AssertException;
import com.thoughtworks.selenium.Selenium;

public abstract class AbstractIntegrationTestSuite2 extends Assert implements
    Selenium {

  public static final String BROWSERBOT = "selenium.browserbot.getCurrentWindow()";

  public static final String SUBMIT = "//input[@type='submit']";

  /**
   * 15 seconds
   */
  public static final String PAGE_LOAD_TIMEOUT = "15000";

  private Selenium selenium;

  private String baseURL;

  protected String getBaseURL() {
    return baseURL;
  }

  @BeforeClass
  public void setup(ITestContext context) {
    selenium = (Selenium) context
        .getAttribute(SeleniumLauncher.SELENIUM_KEY);
    baseURL = (String) context.getAttribute(SeleniumLauncher.BASE_URL_KEY);
  }

  @AfterClass
  public void cleanup() {
    selenium = null;
    baseURL = null;
  }

  @BeforeMethod
  public void indicateTestMethodName(Method testMethod) {
    selenium.setContext(String.format("Running %s: %s", testMethod
        .getDeclaringClass().getSimpleName(), testMethod.getName()
        .replace("_", " ")));
  }

  /* Start of delegate methods */
  public void addCustomRequestHeader(String key, String value) {
    selenium.addCustomRequestHeader(key, value);
  }

  ...
}

Внутри аннотированного метода @ BeforeClass мы получаем тестовый контекст и извлекаем экземпляр селена и базовый URL-адрес, помещенный туда SeleniumLauncher.

Последним фрагментом головоломки является код, который запускает Jetty. Обычно я тестирую свои веб-приложения с помощью плагина Eclipse run-jetty-run , но RJR не поддерживает функциональность «Jetty Plus», включая JNDI. Таким образом, я создал приложение для запуска Jetty:

package com.myclient;

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class RunJetty {

  public static void main(String[] args) throws Exception {

    start().join();
  }

  public static Server start() throws Exception {
    return start(8080);
  }

  public static Server start(int port) throws Exception {
    Server server = new Server(port);

    WebAppContext webapp = new WebAppContext();
    webapp.setContextPath("/");
    webapp.setWar("src/main/webapp");

    // Note: Need jetty-plus and jetty-jndi on the classpath; otherwise
    // jetty-web.xml (where datasources are configured) will not be
    // read.

    server.setHandler(webapp);

    server.start();

    return server;
  }
}

Это все выглядит отлично. Я ожидаю переместить этот код в Tapestry 5.2 довольно скоро. О чем я озадачиваю, так это о нескольких дополнительных идеях:

  • Повышенная гибкость при запуске Jetty, так что вы можете подключить свою собственную конфигурацию сервера Jetty.
  • Возможность запуска нескольких агентов браузера, чтобы один набор тестов мог работать с Internet Explorer, Firefox, Safari и т. Д. Во многих случаях один и тот же метод тестирования может вызываться несколько раз для тестирования на разных браузерах.

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

С http://tapestryjava.blogspot.com