Pull to refresh

Тестирование в Java. TestNG

Reading time 16 min
Views 233K

Наверняка все знакомы с таким понятием как test-driven development(TDD). Наряду с ним также существует такое понятие, как data-driven testing(DDT, не в обиду Шевчуку) — техника написания тестов, при которой данные для тестов хранятся отдельно от самих тестов. Они могут храниться в базе данных, файле, генерироваться во время исполнения теста. Это очень удобно, так как один и тот же функционал тестируется на различных наборах данных, при этом добавление, удаление или изменение этих данных максимально упрощено.

В предыдущей статье я рассмотрел возможности JUnit-а. Там примерами такого рода подхода могут служить запускалки Parameterized и Theories, в обоих случаях один тест-класс может содержать только один такой параметризированный тест(в случае Parameterized несколько, но все они будут использовать одни и те же данные).

В этой статье я заострю внимание на тестовом фреймворке TestNG. Многие уже слышали это название, и перейдя на него, вряд ли желают вернуться к JUnit-у(хотя это только предположение).

Основные возможности


Итак, что же здесь есть? Как и в JUnit 4 тесты описываются с помощью аннотаций, также поддерживаются тесты, написанные на JUnit 3. Есть возможность вместо аннотаций использовать доклет.

Для начала рассмотрим иерархию тестов. Все тесты принадлежат к какой-либо последовательности тестов(сюите), включают в себя некоторое количество классов, каждый из которых может состоять из нескольких тестовых методов. При этом классы и тестовые методы могут принадлежать к определенной группе. Наглядно это выглядит так:
+- suite/
   +- test0/
   |  +- class0/
   |  |  +- method0(integration group)/
   |  |  +- method1(functional group)/
   |  |  +- method2/
   |  +- class1
   |     +- method3(optional group)/
   +- test1/
      +- class3(optional group, integration group)/
         +- method4/

У каждого участника этой иерархии могут иметься before и after конфигураторы. Запускается это все в таком порядке:
+- before suite/
   +- before group/
      +- before test/
         +- before class/
            +- before method/
               +- test/
            +- after method/
            ...
         +- after class/
         ...
      +- after test/
      ...
   +- after group/
   ...
+- after suite/

Теперь поподробнее о самих тестах. Рассмотрим пример. Утилита для работы с локалями, умеет парсить из строки, а также искать кандидаты(en_US -> en_US, en, root):
public abstract class LocaleUtils {

  /**
   * Root locale fix for java 1.5
   */
  public static final Locale ROOT_LOCALE = new Locale("");

  private static final String LOCALE_SEPARATOR = "_";

  public static Locale parseLocale(final String value) {
    if (value != null) {
      final StringTokenizer tokens = new StringTokenizer(value, LOCALE_SEPARATOR);
      final String language = tokens.hasMoreTokens() ? tokens.nextToken() : "";
      final String country = tokens.hasMoreTokens() ? tokens.nextToken() : "";
      String variant = "";
      String sep = "";
      while (tokens.hasMoreTokens()) {
        variant += sep + tokens.nextToken();
        sep = LOCALE_SEPARATOR;
      }
      return new Locale(language, country, variant);
    }
    return null;
  }

  public static List<Locale> getCandidateLocales(final Locale locale) {
    final List<Locale> locales = new ArrayList<Locale>();
    if (locale != null) {
      final String language = locale.getLanguage();
      final String country = locale.getCountry();
      final String variant = locale.getVariant();

      if (variant.length() > 0) {
        locales.add(locale);
      }
      if (country.length() > 0) {
        locales.add((locales.size() == 0) ? locale : new Locale(language, country));
      }
      if (language.length() > 0) {
        locales.add((locales.size() == 0) ? locale : new Locale(language));
      }
    }
    locales.add(ROOT_LOCALE);
    return locales;
  }
}

Напишем к ней тест в стиле JUnit-a(не стоит рассматривать данный пример как руководство к написанию тестов на TestNG):
public class LocaleUtilsOldStyleTest extends Assert {
  private final Map<String, Locale> parseLocaleData = new HashMap<String, Locale>();

  @BeforeClass
  private void setUp() {
    parseLocaleData.put(null, null);
    parseLocaleData.put("", LocaleUtils.ROOT_LOCALE);
    parseLocaleData.put("en", Locale.ENGLISH);
    parseLocaleData.put("en_US", Locale.US);
    parseLocaleData.put("en_GB", Locale.UK);
    parseLocaleData.put("ru", new Locale("ru"));
    parseLocaleData.put("ru_RU_xxx", new Locale("ru", "RU", "xxx"));
  }

  @AfterTest
  void tearDown() {
    parseLocaleData.clear();
  }

  @Test
  public void testParseLocale() {
    for (Map.Entry<String, Locale> entry : parseLocaleData.entrySet()) {
      final Locale actual = LocaleUtils.parseLocale(entry.getKey());
      final Locale expected = entry.getValue();
      assertEquals(actual, expected);
    }
  }
}

Что здесь есть?
  • Как уже было сказано в предыдущей статье я предпочитаю наследовать тест-класс от Assert, это можно заменить статическим импортом, либо использованием класса напрямую(Assert.assertEquals(...)). В реальной системе удобнее всего наследовать тест от какого-либо базового класса, который в свою очередь наследовать от Assert, это дает возможность переопределять либо добавлять необходимые методы. Внимание: в отличие от такого же класса в JUnit здесь во все методы актуальное значение передается первым, ожидаемое вторым(в JUnit наоборот).
  • Аннотации @BeforeSuite, @AfterSuite обозначают методы, которые исполняются единожды до/после исполнения всех тестов. Здесь удобно располагать какие-либо тяжелые настройки общие для всех тестов, например, здесь можно создать пул соединений с базой данных.
  • Аннотации @BeforeTest, @AfterTest обозначают методы, которые исполняются единожды до/после исполнения теста(тот, который включает в себя тестовые классы, не путать с тестовыми методами). Здесь можно хранить настройки какой-либо группы взаимосвязанных сервисов, либо одного сервиса, если он тестируется несколькими тест-классами.
  • Аннотации @BeforeClass, @AfterClass обозначают методы, которые исполняются единожды до/после исполнения всех тестов в классе, идентичны предыдущим, но применимы к тест-классам. Наиболее применим для тестирования какого-то определенного сервиса, который не меняет свое состояние в результате теста.
  • Аннотации @BeforeMethod, @AfterMethod обозначают методы, которые исполняются каждый раз до/после исполнения тестового метода. Здесь удобно хранить настройки для определенного бина или сервиса, если он не меняет свое состояние в результате теста.
  • Аннотации @BeforeGroups, @AfterGroups обозначает методы, которые исполняются до/после первого/последнего теста принадлежащего к заданным группам.
  • Аннотация @Test обозначает сами тесты. Здесь размещаются проверки. Также применима к классам

У всех этих аннотаций есть следующие параметры:
  • enabled — можно временно отключить, установив значение в false
  • groups — обозначает, для каких групп будет исполнен
  • inheritGroups — если true(а по умолчанию именно так), метод будет наследовать группы от тест-класса
  • timeOut — время, после которого метод «свалится» и потянет за собой все зависимые от него тесты
  • description — название, используемое в отчете
  • dependsOnMethods — методы, от которых зависит, сначала будут выполнены они, а затем данный метод
  • dependsOnGroups — группы, от которых зависит
  • alwaysRun — если установить в true, будет вызываться всегда независимо от того, к каким группам принадлежит, не применим к @BeforeGroups, @AfterGroups
Как видно из примера тест практически ничем не отличается от такого же теста на JUnit. Если нет разницы, то зачем использовать TestNG?

Параметризированные тесты


Напишем этот же тест другим способом:
public class LocaleUtilsTest extends Assert {

  @DataProvider
  public Object[][] parseLocaleData() {
    return new Object[][]{
      {null, null},
      {"", LocaleUtils.ROOT_LOCALE},
      {"en", Locale.ENGLISH},
      {"en_US", Locale.US},
      {"en_GB", Locale.UK},
      {"ru", new Locale("ru")},
      {"ru_RU_some_variant", new Locale("ru", "RU", "some_variant")},
    };
  }

  @Test(dataProvider = "parseLocaleData")
  public void testParseLocale(String locale, Locale expected) {
    final Locale actual = LocaleUtils.parseLocale(locale);
    assertEquals(actual, expected);
  }
}

Проще? Конечно, данные хранятся отдельно от самого теста. Удобно? Конечно, можно добавлять тесты, добавляя всего лишь строчку в метод parseLocaleData.

Итак, как это работает?
  • Объявляем тестовый метод со всеми нужными ему параметрами, например входные и ожидаемые данные. В нашем случае это строка, которую нужно распарсить в локаль и ожидаемая в результате локаль.
  • Объявляем дата провайдер, хранилище данных для теста. Обычно это метод, возвращающий Object[][] либо Iterator<Object[]>, содержащий список параметров для определенного теста, например {«en_US», Locale.US}. Этот метод должен быть зааннотирован с помощью @DataProvider, в самом тесте он объявляется с помощью параметра dataProvider в аннотации @Test. Также можно указать имя(параметр name), если не указывать в качестве имени будет использоваться название метода.

Еще один пример, теперь разнесем данные и логику теста в разные классы:
public class LocaleUtilsTestData {

  @DataProvider(name = "getCandidateLocalesData")
  public static Object[][] getCandidateLocalesData() {
    return new Object[][]{
      {null, Arrays.asList(LocaleUtils.ROOT_LOCALE)},
      {LocaleUtils.ROOT_LOCALE, Arrays.asList(LocaleUtils.ROOT_LOCALE)},
      {Locale.ENGLISH, Arrays.asList(Locale.ENGLISH, LocaleUtils.ROOT_LOCALE)},
      {Locale.US, Arrays.asList(Locale.US, Locale.ENGLISH, LocaleUtils.ROOT_LOCALE)},
      {new Locale("en", "US", "xxx"), Arrays.asList(
        new Locale("en", "US", "xxx"), Locale.US, Locale.ENGLISH, LocaleUtils.ROOT_LOCALE)
      },
    };
  }
}

public class LocaleUtilsTest extends Assert {
  // other tests

  @Test(dataProvider = "getCandidateLocalesData", dataProviderClass = LocaleUtilsTestData.class)
  public void testGetCandidateLocales(Locale locale, List<Locale> expected) {
    final List<Locale> actual = LocaleUtils.getCandidateLocales(locale);
    assertEquals(actual, expected);
  }
}

В этом случае задаются параметры dataProviderClass и dataProvider. Метод, возвращающий тестовые данные должен быть static.

Кроме описанного выше есть еще один способ параметризировать тесты. Нужный метод аннотируется с помощью @Parameters, где указываются имена всех необходимых параметров. Некоторые из параметров можно зааннотировать с помощью @Optional с указанием значения по умолчанию(если не указать, то будут использоваться значения по умолчанию для примитивов, либо null для всех остальных типов). Значения параметров хранятся в конфигурации TestNG(которая будет рассмотрена позже). Пример:
public class ParameterizedTest extends Assert {
  private DataSource dataSource;

  @Parameters({"driver", "url", "username", "password"})
  @BeforeClass
  public void setUpDataSource(String driver, String url, @Optional("sa") String username, @Optional String password) {
    // create datasource
    dataSource = ...
  }

  @Test
  public void testOptionalData() throws SQLException {
    dataSource.getConnection();
    // do some staff
  }
}

В данном случае метод setUpDataSource будет принимать в качестве параметров настройки соединения с БД, причем параметры username и password опциональны, с заданными значениями по умолчанию. Очень удобно использовать с данными, общими для всех тестов(ну или почти всех), например, как в примере настройки соединения с БД.

Ну и в завершение следует сказать пару слов о фабриках, которые позволяют создавать тесты динамически. Также, как и сами тесты, могут быть параметризированы с помощью @DataProvider либо @Parameters:
public class FactoryTest {

  @DataProvider
  public Object[][] tablesData() {
    return new Object[][] {
      {"FIRST_TABLE"},
      {"SECOND_TABLE"},
      {"THIRD_TABLE"},
    };
  }

  @Factory(dataProvider = "tablesData")
  public Object[] createTest(String table) {
    return new Object[] { new GenericTableTest(table) };
  }
}

public class GenericTableTest extends Assert {
  private final String table;

  public GenericTableTest(final String table) {
    this.table = table;
  }

  @Test
  public void testTable() {
    System.out.println(table);
    // do some testing staff here
  }
}

Вариант с @Parameters:
public class FactoryTest {

  @Parameters("table")
  @Factory
  public Object[] createParameterizedTest(@Optional("SOME_TABLE") String table) {
    return new Object[] { new GenericTableTest(table) };
  }
}

Многопоточность


Нужно проверить, как поведет себя приложение во многопоточном окружении? Можно сделать так, чтобы тесты исполнялись одновременно из нескольких потоков:
public class ConcurrencyTest extends Assert {
  private Map<String, String> data;

  @BeforeClass
  void setUp() throws Exception {
    data = new HashMap<String, String>();
  }

  @AfterClass
  void tearDown() throws Exception {
    data = null;
  }

  @Test(threadPoolSize = 30, invocationCount = 100, invocationTimeOut = 10000)
  public void testMapOperations() throws Exception {
    data.put("1", "111");
    data.put("2", "111");
    data.put("3", "111");
    data.put("4", "111");
    data.put("5", "111");
    data.put("6", "111");
    data.put("7", "111");
    for (Map.Entry<String, String> entry : data.entrySet()) {
      System.out.println(entry);
    }
    data.clear();
  }

  @Test(singleThreaded = true, invocationCount = 100, invocationTimeOut = 10000)
  public void testMapOperationsSafe() throws Exception {
    data.put("1", "111");
    data.put("2", "111");
    data.put("3", "111");
    data.put("4", "111");
    data.put("5", "111");
    data.put("6", "111");
    data.put("7", "111");
    for (Map.Entry<String, String> entry : data.entrySet()) {
      System.out.println(entry);
    }
    data.clear();
  }
}

  • threadPoolSize определяет максимальное количество потоков используемое для тестов.
  • singleThreaded если установлен в true все тесты будут запущены в одном потоке.
  • invocationCount определяет количество запусков теста.
  • invocationTimeOut определяет общее время всех запусков теста, после которого тест считается провалившемся.
Первый тест будет время от времени проваливаться с ConcurrentModificationException, так как будет запускаться из разных потоков, второй — нет, так как все тесты будут запущены последовательно из одного потока.

Еще можно установить параметр parallel у дата провайдера в true, тогда тесты для каждого набора данных будут запущены паралельно, в отдельном потоке:
public class ConcurrencyTest extends Assert {
  // some staff here

  @DataProvider(parallel = true)
  public Object[][] concurrencyData() {
    return new Object[][] {
      {"1", "2"},
      {"3", "4"},
      {"5", "6"},
      {"7", "8"},
      {"9", "10"},
      {"11", "12"},
      {"13", "14"},
      {"15", "16"},
      {"17", "18"},
      {"19", "20"},
    };
  }

  @Test(dataProvider = "concurrencyData")
  public void testParallelData(String first, String second) {
    final Thread thread = Thread.currentThread();
    System.out.printf("#%d %s: %s : %s", thread.getId(), thread.getName(), first, second);
    System.out.println();
  }
}

Данный тест будет выводить нечто вроде:
#16 pool-1-thread-3: 5 : 6
#19 pool-1-thread-6: 11 : 12
#14 pool-1-thread-1: 1 : 2
#22 pool-1-thread-9: 17 : 18
#20 pool-1-thread-7: 13 : 14
#18 pool-1-thread-5: 9 : 10
#15 pool-1-thread-2: 3 : 4
#17 pool-1-thread-4: 7 : 8
#21 pool-1-thread-8: 15 : 16
#23 pool-1-thread-10: 19 : 20

Без этого параметра будет что-то вроде:
#1 main: 1 : 2
#1 main: 3 : 4
#1 main: 5 : 6
#1 main: 7 : 8
#1 main: 9 : 10
#1 main: 11 : 12
#1 main: 13 : 14
#1 main: 15 : 16
#1 main: 17 : 18
#1 main: 19 : 20

Дополнительные возможности


Кроме всего описанного есть и другие возможности, например для проверки выброса исключений(очень удобно использовать для тестов на неправильных данных):
public class ExceptionTest {

  @DataProvider
  public Object[][] wrongData() {
    return new Object[][] {
      {"Hello, World!!!"},
      {"0x245"},
      {"1798237199878129387197238"},
    };
  }

  @Test(dataProvider = "wrongData", expectedExceptions = NumberFormatException.class,
      expectedExceptionsMessageRegExp = "^For input string: \"(.*)\"$")
  public void testParse(String data) {
    Integer.parseInt(data);
  }
}

  • expectedExceptions задает варианты ожидаемых исключений, если они не выбрасываются, тест считается провалившемся.
  • expectedExceptionsMessageRegExp то же что и предыдущий параметр, но задает regexp для сообщения об ошибке.
Еще один пример:
public class PrioritiesTest extends Assert {
  private boolean firstTestExecuted;

  @BeforeClass
  public void setUp() throws Exception {
    firstTestExecuted = false;
  }

  @Test(priority = 0)
  public void first() {
    assertFalse(firstTestExecuted);
    firstTestExecuted = true;
  }

  @Test(priority = 1)
  public void second() {
    assertTrue(firstTestExecuted);
  }
}

  • priority определяет приоритет теста внутри класса, чем меньше, тем раньше будет выполнен.
Данный пример пройдет успешно, так как сначала выполнится метод first, затем second. Если поменять приоритет у first на 2, тест провалится.

Похожее поведение будет наблюдаться также если указать зависимости у теста, например, добавим в наш тест:
public class PrioritiesTest extends Assert {
  // some staff

  @Test(dependsOnMethods = {"first"})
  public void third() {
    assertTrue(firstTestExecuted);
  }

  // some staff
}

Обычно это удобно, когда один тест зависит от другого, например, Утилита1 использует Утилиту0, если Утилита0 работает неправильно, то нет смысла тестировать Утилиту1. С другой стороны зависимости также удобно использовать в @Before, @After методах, особенно для связи базового теста с наследующимся, причем иногда бывает необходимо сделать так, чтобы даже если метод A свалился, а метод B зависит от него, метод B все равно вызывался. В этом случае устанавливаем параметр alwaysRun в true.

Внедрение зависимостей


Хочу порадовать любителей фреймворка от «корпорации добра» Guice. В TestNG есть встроенная поддержка последнего. Выглядит это так:
public class GuiceModule extends AbstractModule {

  @Override
  protected void configure() {
    bind(String.class).annotatedWith(Names.named("guice-string-0")).toInstance("Hello, ");
  }

  @Named("guice-string-1")
  @Inject
  @Singleton
  @Provides
  public String provideGuiceString() {
    return "World!!!";
  }
}

@Guice(modules = {GuiceModule.class})
public class GuiceTest extends Assert {

  @Inject
  @Named("guice-string-0")
  private String word0;

  @Inject
  @Named("guice-string-1")
  private String word1;

  @Test
  public void testService() {
    final String actual = word0 + word1;
    assertEquals(actual, "Hello, World!!!");
  }
}

Все, что надо — зааннотировать нужный класс с помощью @Guice и указать в параметре modules все необходимые guice-модули. Далее в тест классе можно уже использовать внедрение зависимостей, используя @Inject.

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

Расширение функционала


Расширение функционала может быть реализовано с помощью механизма слушателей. Поддерживаются следующие типы слушателей:
  • IAnnotationTransformer, IAnnotationTransformer2 — позволяют переопределять настройки теста, например, количество потоков для запуска теста, таймаут, ожидаемое исключение:
    public class ExpectTransformer implements IAnnotationTransformer {
      public void transform(ITestAnnotation annotation, Class testClass, Constructor testConstructor, Method testMethod) {
        if (testMethod.getName().startsWith("expect")) {
          annotation.setExpectedExceptions(new Class[] {Exception.class});
        }
      }
    }
    

    Данный пример будет ожидать выброс исключения от тест-методов, начинающихся с expect.
  • IHookable — позволяет переопределить тест-метод или по возможности пропустить, в туториале к TestNG приводится пример с JAAS.
  • IInvokedMethodListener, IInvokedMethodListener2 — похож на предыдущий слушатель, но исполняет код до и после исполнения тест-метода
  • IMethodInterceptor — позволяет изменять порядок запуска тестов(применим только к тестам, которые независимы от других тестов). В туториале есть хороший пример
  • IReporter — позволяет расширить функционал, выполняемый после выполнения всех тестов, обычно этот функционал связан с генерацией отчетов об ошибках и т.д. Таким образом можно реализовать свой механизм отчетов
  • ITestListener — слушатель, который может обрабатывать большинство событий от тест-метода, например, start, finish, success, failure
  • ISuiteListener — похож на предыдущий, но для сюит, получает только события start и finish
Об одном из примеров интересного использования механизма слушателей можно почитать здесь.

Конфигурация


Теперь перейдем к конфигурации тестов. Простейший способ запустить тесты выглядит примерно так:
  final TestNG testNG = new TestNG(true);
  testNG.setTestClasses(new Class[] {SuperMegaTest.class});
  testNG.setExcludedGroups("optional");
  testNG.run();

Но чаще всего для запуска тестов используется XML либо YAML конфигурация. XML конфигурация выглядит примерно так:
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Test suite" parallel="classes" thread-count="10">
  <test name="Default tests" verbose="1" annotations="JDK" thread-count="10" parallel="classes">
    <parameter name="driver" value="com.mysql.jdbc.Driver"/>
    <parameter name="url" value="jdbc:mysql://localhost:3306/db"/>

    <groups>
      <run>
        <exclude name="integration"/>
      </run>
    </groups>

    <packages>
      <package name="com.example.*"/>
    </packages>
  </test>

  <test name="Integration tests" annotations="JDK">
    <groups>
      <run>
        <include name="integration"/>
      </run>
    </groups>

    <packages>
      <package name="com.example.*"/>
    </packages>
  </test>
</suite>

Аналогичная YAML конфигурация:
name: YAML Test suite
parallel: classes
threadCount: 10
tests:
  - name: Default tests
    parameters:
      driver: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db
    excludedGroups: [ integration ]
    packages:
      - com.example.*
  - name: Integration tests
    parameters:
      driver: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/db
    includedGroups: [ integration ]
    packages:
      - com.example.*

Тогда для запуска тестов нужно будет сделать следующее:
  final TestNG testNG = new TestNG(true);
  //final Parser parser = new Parser("testing/testing-testng/src/test/resources/testng.xml");
  final Parser parser = new Parser("testing/testing-testng/src/test/resources/testng.yaml");
  final List<XmlSuite> suites = parser.parseToList();
  testNG.setXmlSuites(suites);
  testNG.run();

Хотя, наверное, программный запуск тестов это излишне, т.к. для этого можно использовать возможности IDE.

Вернемся к самой конфигурации. На самом верхнем уровне настраивается последовательность тестов(сюита). Может принимать следующие параметры:
  • name — название используемое в отчете
  • thread-count — количество потоков используемое для запуска тестов
  • data-provider-thread-count — количество потоков используемое для передачи данных из дата-провайдеров в сами тесты для параллельных дата провайдеров(@DataProvider(parallel = true))
  • parallel — может принимать следующие значения:
  • methods — тестовые методы будут запущены в разных потоках, нужно быть осторожным если есть зависимости между методами
  • classes — все методы одного класса в одном потоке, но разные классы в разных потоках
  • tests — все методы одного теста в одном потоке, разные тесты в разных потоках
  • time-out — время, после которого тест будет считаться провалившимся, то же что и в аннотации, но распостраняется на все тестовые методы
  • junit — JUnit 3 тесты
  • annotations — если javadoc, то будет использован доклет для конфигурации
Также могут быть настроены:
  • parameter — параметры, те, что используются в @Parameters
  • packages — пакеты, где искать тест-классы
  • listeners — слушатели, с их помощью можно расширить функционал TestNG, о них уже сказал пару слов
  • method-selectors — селекторы для тестов, должны реализовывать интерфейс IMethodSelector
  • suite-files — можно включать другие файлы конфигурации
Сюиты в свою очередь могут включать в себя тесты с практически такими же настройками, что и для сюит (аттрибуты name, thread-count, parallel, time-out, junit, annotations, тэги parameter, packages, method-selectors). Также у тестов имеются и своеобразные настройки, например, запускаемые группы:
  <test name="Default tests" verbose="1" annotations="JDK" thread-count="10" parallel="classes">
    <!-- some staff here -->

    <groups>
      <run>
        <exclude name="integration"/>
      </run>
    </groups>

    <!-- some staff here -->
  </test>

В данном примере тест будет включать в себя только тесты не относящиеся к группе integration.

Еще тесты могут включать в себя тест-классы, которые в свою очередь могут включать/исключать в себя тест-методы.
  <test name="Integration tests">
    <groups>
      <run>
        <include name="integration"/>
      </run>
    </groups>

    <classes>
      <class name="com.example.PrioritiesTest">
        <methods>
          <exclude name="third"/>
        </methods>
      </class>
    </classes>
  </test>


Вывод


Это не все, что можно сказать по этому замечательному фреймворку, все охватить в одной статье очень трудно. Но я постарался раскрыть основные моменты его использования. Тут, конечно, можно было рассказать немного больше про механизм слушателей, про интеграции с другими фреймворками, про конфигурацию тестов, но тогда статья превратилась бы в книгу, да и не знаю я всего. Но, надеюсь, эта статья была кому-то полезна и то, что было сказано, сподвигнет некоторых читателей на использование TestNG, или хотя бы на DDT технику написания тестов.

Примеры можно найти здесь, различные статьи здесь.

Литература


Tags:
Hubs:
+22
Comments 22
Comments Comments 22

Articles