Говнокодер
0,0
рейтинг
13 июня 2011 в 20:34

Разработка → Тестирование в Java. TestNG tutorial

TDD*, JAVA*

Наверняка все знакомы с таким понятием как 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 технику написания тестов.

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

Литература


Иван Холопик @sody
карма
50,0
рейтинг 0,0
Говнокодер
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (22)

  • 0
    Хорошие тесты. Напишу, как пишут многие — это для простых функций! Всегда пугало не само юнит-тестирование, а работа со сложными, разветвленными и зависимыми классами. При этом всегда приводятся простейшие примеры. Предлагаю описать некоторое сложное приложение и стратегию его тестирования с использованием DDT и TDD, с использованием mosk-объектов и всех преимуществ тестов. Хочется оценить объемы тестов и тестируемого кода.
    • +3
      Прошу не считать данный ответ хамством :)

      За несколько последний лет я убедился, что если какой-то код (или кусок кода) заставляет думать, как же его тестировать, то он неправильно написан*. Вот прямо сейчас я сижу и переделываю чужой код, который плохо тестируется (именно по указанной Вами причине — сложные, разветвленные и зависимые классы).

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

      Прошу мне поверить, что я не упертый пурист и не догматик. Для меня TDD — не догма. Я нормальный инженер, и моя главная задача, чтобы мой код хорошо работал. Просто я много-много раз убеждался, что много хороших тестов — это гарантия того, что код будет работать хорошо и сегодня, и через полгода, и когда угодно. И если его изменить/улучшить, то тесты поймают все потенциальные проблемы, значит лично мне не придется отвлекаться от хабра или чем там я еще буду заниматься, чтобы выискивать баги в старом (или, еще хуже) чужом коде. Поэтому я сам пишу, как минимум, в три раза больше строк кода в тестах, чем в собственно продукте, и от своих орлов требую того же.

      К чему я снова принялся доказывать, что тесты — это хорошо? А к тому, что:
      1. Тесты — это хорошо
      2. Если код нетривиально тестируется, значит, это плохой код, который надо переделать. Почему? См. 1.

      *) Исключение составляют куски кода, которые взаимодействуют с внешними вещами — чужой онлайновый сервис, запущенные процессы, и т.д., и т.п. В этом случае тестирование действительно нетривиально, но возможно. Для этого берется самый лучший инженер в команде и он пишет фрейворк для тестирование именно таких кусков (например, собственная имплементация Amazon EC2 API. Или библиотека для запуска и гарантированного уничтожения внешних процессов. Или еще что-нибудь такое же ненужное и громоздкое. Потому что оно нужное. Может быть, самое нужное во всем продукте.

      Где-то так
      • 0
        Хороший ответ, только:
        1. Весь код, что тебе встретился не переделать;
        2. Тесты для 12000 классов лишь в одном проекте из нескольких десятков проектов как то сложно себе представить;
        3. Как быть, если в коде нет расчетного кода, а есть лишь взаимодействующий с внешними системами?

        Я не против тестирования, я за. Но обычно в наследство достаются огромные системы без тестов.
        • 0
          Для унаследованного кода — да, трудно.

          Но если писать с нуля и одновременно код и тесты, то это не только возможно, но и желательно.

          В моем последнем проекте лично мы написали около 200000 строк кода, покрытие тестами колебалось между 75 и 85 процентами. Не было тестов на всяки геттеры/сеттеры и прочий boilerplate код. Я поспорил с менеджером на обед, что профессиональные QA не найдут багов, и выиграл (они, конечно, нашли, но это были проблемы с юзабилити и не до конца ясными требованиями. Наш код работал.)
    • 0
      Просили — получите
    • +1
      Также прошу не считать ответ хамством, это всего лишь мое мнение :)

      Целью для этой статьи ставил обзор именно TestNG, не охватывая при этом другие фреймворки(собираюсь рассмотреть их в следующих статьях). Да, примеры тестов просты, но если бы я использовал сложные примеры, многим они были бы непонятны.

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

      1. Тесты для отдельных компонентов. Все(или почти все) связи заменяются на мок-объекты, тестируется каждый предоставляемый метод(или что там еще может быть). У метода есть входные параметры и ожидаемый результат. Таким образом, эти тесты сводятся к простым, создаем набор данных (входные параметры, ожидаемый результат) и тестируем на этих данных метод.

      2. Тесты для групп компонентов либо модулей в целом. Здесь используются реальные компоненты, на мок-объекты заменяются компоненты из других групп/модулей. Принцип такой же как и в предыдущем примере.

      3. Тесты для всей системы в целом. Здесь уже используются только реальные компоненты, причем могут прогоняться в различных средах(различные апп-сервера, различные БД). Тот же принцип, все тесты сводятся к простым.

      Конечно, архитектура приложения может быть сложной иногда даже пластилиновой, и это повлечет за собой создание такой же сложной архитектуры тестов, которая скорее всего будет находится в базовых классах в методах before и after, и будет использоваться многократно для тестирования различных компонентов. Но таккая архитектура тестов создается однажды и дальнейшее написание тестов сводится к вышеописанным действиям.
  • 0
    Неофитов TestNG хочу предупредить — существует определенный геморрой при запуске тестов TestNG через maven-surefire-plugin. В энторнетах примерно миллион блогов, посвященных этому вопросу, ищите и найдете, какую версию плагина использовать с какой версией TestNG. Ничего фатального, просто некоторые версии этих вещей несовместимы.
    • 0
      Я и мои коллеги предпочитаем использовать JUnit вместо NG именно по этой причине — плохая поддержка со стороны maven. Плюс плагин к Eclipse у JUnit лучше.
      • 0
        К счастью с данными проблемами не знаком, возможно потому что использую всегда последнюю версию обоих. Насчет плохой поддержки со стороны мавена не согласен, что такого особенного у поддержки JUnit-а, чего нет у TestNG?
      • 0
        Эээ… а почему «вместо»? У нас, например, куча тестов на JUnit3, JUnit4, TestNG, все вместе прекрасно работает. В зависимости от потребности используем наиболее подходящий инструмент.

        Разобраться с очередной несовместимостью версий — час от силы, требуется это сделать один или два раза за весь проект.
        • 0
          Потому что если проект новый, то проще использовать одну библиотеку, а не 2-3.
          А какими IDE и какими плагинами вы пользуетесь для запуска этого хозяйства?
          • +1
            Я пользуюсь идеей, поддержка TestNG отличная, иногда запускаю в ней тесты через мавен, есть возможность дебажить запущеные так тесты.

            Плагины для мавена:
            maven-surefire-plugin — без комментариев
            maven-failsafe-plugin — интеграционные тесты, запускаются после сборки модуля.
            maven-invoker-plugin — для запуска отдельного проекта с тестами(запускаемыми первыми двумя плагинами), полезно для тестов в различной среде исполнения, с различными зависимотсями и т.д.
            • 0
              Ок, спасибо.
              Неочевиден invoker — его я не использовал еще, и на первый взгляд он выглядит удобно, когда интеграционные тесты лежат в отдельном проекте/модуле (особенно когда их пишет отдельная от разработчиков группа QA).

              Вообще я имел в виду именно плагины к IDE, которые могут красиво графически показать зелененькие и красненькие полоски одним тычком мыши, без необходимости создания Run Configuration в Eclipse, например. Для проекта или отдельного класса. С мавеном-то проблем обычно не возникает. Я использовал плагин для TestNG (так и называется — «TestNG plug-in for Eclipse»). Так и не научил его нормально работать без xml-конфига тестовых сьютов, остановился на ран конфигах для mvn test. Плагин для JUnit, судя по тому что я о нем помню, таких проблем не имеет — и для проекта, и для класса все запускается одним тычком.
              • 0
                В идее все проще, контекстное меню -> запустить или продебажить тест-метод(тест-класс) -> создается временный xml-ничек во временной папочке и тесты пошли.
              • 0
                Честно говоря, я не очень понимаю, что означает «нормально работать без xml-конфига». Он что, не работал просто через Alt-Shift-X, N? Или «Run As..» -> «TestNG»? А как у вас тестовый класс назывался?
          • 0
            Оно, конечно, правильно, но вы упускаете существование кучи библиотек дл я тестирования, имеющий в своей основе JUnit 3, 4, или TestNG.

            IDE — Эклипс, плагин для TestNG, и все прекрасно запускается через Alt-Shift-X, N (TestNG) или Alt-Shit-X, T (JUnit любой версии).

            В мавене используется maven-surefire-plugin, в котором тоже все просто работает (если соблюдены конвенции названия тестов). Пару раз бывало, что какая-то версия surefire не работала с какой-то конкретной версией TestNG, но это все решаемо.
  • 0
    А как в TestNG делается то, что в JUnit 4 реализуется с помощью @org.junit.runner.RunWith?
  • 0
    Экую картинку вы выбрали для фреймворка тестирования :) Не очень стимулирует :).
    Тесты это хорошо. Полностью согласен с вами.
    Тесты сложного функционала это очень хорошо, но на их реализацию уходит нередко чуть ли не в три раза больше времени, чем на реализацию непосредственно самого кода. Посему нередко эти куски и обходят стороной и проверяют работоспособность на рабочем проекте уже.
  • 0
    Второй десяток третьего тысячелетия. Тесты стали настолько сложными, а их конфигурация настолько нетривиальная, что пришлось писать тесты для тестов, используя более старые, но более простые фреймворки )))
    • 0
      «в каждой шутке есть только доля шутки» :)

      Раз уж вы решили использовать тесты (кроме простых а-ля проверяем, правильно ли выполняется валидация e-mail адреса), то относитесь к ним как к основному коду: если проект довольно большой и предполагает долгую жизнь, в течении которой требования будут меняться (а они практически всегда меняются), то тесты нужно проектировать, а не лепить как попало (продумывать иерархию классов, архитектуру и всё остальное). Тогда и на изменения будет легче реагировать.

      Тут ведь главное что: поменялись требования — правим код и тесты. Если просто рефакторинг или багфиксинг (в 99%) — тесты не трогаем.
  • 0
    Кстати, было бы интересно собрать статистику — какую из двух библиотек (JUnit или TestNG) люди предпочли бы при разработке нового проекта с нуля и, соответственно, почему.

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