DataSupplier - entity-driven альтернатива TestNG DataProvider'у

Вашему вниманию предлагается библиотека-расширение стандартного TestNG DataProvider механизма - test-data-supplier.

Пользователям TestNG, пожалуй, не стоит рассказывать о тех неудобствах, которые приходится испытывать при работе с DataProvider. Не смотря на прекрасную идею, реализация самого механизма и отсутствие поддержки Java 8 лишает нас всякой гибкости при работе данными. В современном Java мире все уже давно пользуются Collections / Streams, а не убогими двумерными массивами / итераторами.

Что же предлагает test-data-supplier? Во-первых, расширенную поддержку типов возвращаемых значений. На текущий момент это:

  • Collection (любые типы-наследники)
  • Object[] (любые типы)
  • double[]
  • int[]
  • long[]
  • Stream (кроме специализированных)
  • StreamEx (кроме специализированных)
  • Любой неитерируемый стандартный или пользовательский тип

Если, к примеру, нам нужно осуществить какую-то сложную фильтрацию данных до непосредственной поставки в тест, можно использовать следующий синтаксис:

@DataSupplier
public Stream<User> getData() {
    return Stream.of(
        new User("Petya", "password2"),
        new User("Virus Petya", "password3"),
        new User("Mark", "password1"))
            .filter(u -> !u.getName().contains("Virus"))
            .sorted(comparing(User::getPassword));
}
    
@Test(dataProvider = "getData")
public void shouldSupplyStreamData(final User user) {
    // ...
}

Где DataSupplier - аннотация-альтернатива DataProvider’у.
На текущий момент доступна лишь одна опция - extractValues = true / false (default), позволяющая раскладывать возвращаемую коллекцию / стрим на input аргументы теста. Т.е. если мы хотим запустить тест лишь раз, передав N аргументов на вход, можно воспользоваться следующей конструкцией:

@DataSupplier(extractValues = true)
public List<User> getExtractedData() {
    return StreamEx.of(
        new User("username1", "password1"),
        new User("username2", "password2"))
            .toList();
}
        
@Test(dataProvider = "getExtractedData")
public void shouldSupplyExtractedListData(final User... users) {
    // ...
}

При этом, оба юзера попадут на вход тесту, вместо 2 независимых запусков с 1 юзером.

Если нам нужна не коллекция / стрим, а всего один объект (для очень простых тестов):

@DataSupplier
public String getData() {
    return "username";
}
    
@Test(dataProvider = "getData")
public void shouldSupplySingleObject(final String username) {
    // ...
}

В самой аннотации Test все остается без изменений. Но ссылаемся мы уже не на DataProvider, а на DataSupplier метод.

К сожалению, TestNG plugin inspections пока будут выдавать warnings о неиспользуемых методах и отсутствующих data providers. Частично это лечится на уровне IDE. Но по-хорошему, нужно либо обновлять соответствующий плагин, либо писать новый. На текущий момент этого нет в планах.

Поддержки специфических фич DataProvider (по типу параллелизации / кастомных имен) нет, и скорее всего не будет. За N лет работы с TestNG я не нашел им должного применения. Если у кого-то есть весомые аргументы “за их использование”, можем обсудить.

Помимо dependency, для использования библиотеки нужно подключить еще и спец. listener → io.github.sskorol.dataprovider.DataProviderTransformer.

По приведенной выше линке можно найти более детальную информацию и ссылки на sample проекты для maven / gradle + примеры тестов в основном репозитории.

Хотелось бы еще заметить, что библиотека является всего лишь оберткой, а не принципиально новой реализацией DataProvider. Т.е. все баги / специфическое поведение оного будут проявляться и у DataSupplier.

Буду признателен за фидбек / issues, т.к. библиотека еще не обкатана.

П.С. Лицензия Apache 2.0, монетизации не будет. Изначально разработана для личного использования, теперь в паблике.

You’re welcome!

17 лайков

С меня лайк и подписка

1 лайк

Небольшой update: не так давно была опубликована версия 1.0.0 c поддержкой всех фич стандартного DataProvider (кроме индексов, которые легко заменяются фильтрами в стримах).

Помимо этого, была добавлена 0.0.1 версия IntelliJ IDEA плагина, который устанавливает связь между:

@Test(dataProvider = "getData")

и DataSupplier методом:

@DataSupplier
public String getData() {
    return "data";
}

Отсутствующие методы будут отмаркированы красным цветом. При клике на имя, указанное в качестве dataProvider arg, осуществляется навигация к соответствующему методу (поддерживаются также варианты с заданием кастомных имен).

Для корректной работы плагина необходимо отключить встроенные TestNG inspections → Data provider problems.

Плагин доступен в официальном JetBrains репозитории, и может быть загружен прямо из IDE.

2 лайка

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

public class DataSupplierInterceptorImpl implements DataSupplierInterceptor {
    
    private static final Map<Method, DataSupplierMetaData> META_DATA = new ConcurrentHashMap<>();
    
    @Override
    public void beforeDataPreparation(final ITestContext context, final Method method) {
    }
    
    @Override
    public void afterDataPreparation(final ITestContext context, final Method method) {
    }
    
    @Override
    public void onDataPreparation(final DataSupplierMetaData testMetaData) {
        META_DATA.putIfAbsent(testMetaData.getTestMethod(), testMetaData);
    }
    
    @Override
    public Collection<DataSupplierMetaData> getMetaData() {
        return META_DATA.values();
    }
}
4 лайка

В версии 1.2.0 был немного переработан механизм трансформации.
extractValues был разбит на 2 флага: transpose и flatMap.

Первый фактически делает то же, что и предшественник - транспонирует вектор столбец в строку: вместо N итераций мы получаем 1 итерацию с N объектами на вход тесту. Т.е. если у нас есть StreamEx.of("a", "b", "c"), и мы не хотим запускать тест 3 раза (каждый раз передавая 1 стринговый аргумент на вход), включенный флаг обеспечит передачу сразу 3х аргументов на вход при единственной итерации.

Второй флаг - аналог Java-like flatMap операции на стриме. Т.е. если у нас был, к примеру List<List<String>>, и мы хотим запустить тест externalList.size() раз, передавая не вложенный список на вход, а именно его содержимое (internalList.size() аргументов), то это как раз то, что произойдет при использовании flatMap.

Помимо этого, появилась возможность работы с Map / Entry типами возвращаемых значений, аля:

@DataSupplier(flatMap = true)
public Map<Integer, String> getInternallyExtractedMapData() {
    return EntryStream.of(asList("user3", "user4")).toMap();
}
    
@Test(dataProvider = "getInternallyExtractedMapData")
public void supplyInternallyExtractedMapData(final Integer key, final String value) {
    // ...
}

П.С. Кто уже попробовал библиотеку, и вам она понравилась, не забывайте ставить звезды на GitHub, чтобы быстрей продвинуть ее в массы. :wink:

4 лайка

Давненько не постил апдейтов. Спустя несколько релизов, библиотека пополнилась следующими фичами / фиксами:

  • поддержка class level аннотаций;
  • для безболезненной миграции была все же добавлена поддержка indexes[]; так что теперь @DataSupplier имеет полный набор фич оригинального @DataProvider;
  • добавлена поддержка @Factory;
  • вместо Method в сигнатуру можно теперь инжектить более гибкую структуру - ITestNGMethod;
  • слушатель теперь грузится автоматически посредством SPI; следовательно, его уже не нужно явно подключать в билд файле; т.е. фактически, библиотека работает из коробки при добавлении одной лишь зависимости;
  • добавлена возможность задания кастомных IAnnotationTransformer через SPI (ввиду того, что TestNG запрещает определять более одного, а в проекте уже один задействован);
  • плагин для IntelliJ стал так же более умным в плане поиска сложных зависимостей через наследование
  • ну и наконец, в последней версии добавлены хэлперы и аннотации для чтения JSON / CSV файлов (даже по URL) в java сущности.

Допустим у нас есть следующие данные в classpath:

[
  {
    "username": "admin",
    "password": "admin"
  },
  {
    "username": "test",
    "password": "password"
  },
  {
    "username": "guest",
    "password": "123"
  }
]
username,password
"admin","admin"
"test","password"
"guest","123"

А нам хотелось бы вычитать их прямо в Java entity. Достаточно воспользоваться аннотацией @Source, принимающей либо имя файла ресурсов, либо URL. Если же в именах полей существуют расхождения, можно воспользоваться соответствующим маппингом, используя аннотации @FieldName для CSV, и @SerializedName для JSON.

@Data
@Source(path = "users.json")
public class User {

    @SerializedName("username")
    private final String name;
    private final String password;
}
@Data
@Source(path = "users.csv")
public class User {

    @FieldName("username")
    private final String name;
    private final String password;
}
@Data
@Source(path = "https://raw.githubusercontent.com/LearnWebCode/json-example/master/animals-1.json")
public class Animal {

    private final String name;
    private final String species;
    private final Food foods;
}
@Data
@Source(path = "http://samplecsvs.s3.amazonaws.com/SacramentocrimeJanuary2006.csv")
public class CrimeRecord {

    private final String address;
    @FieldName("crimedescr")
    private final String description;
}

Далее достаточно в @DataSupplier обратиться к утилитным методам getJsonRecords / getCsvRecords, передав туда класс, куда бы хотелось записать результаты.

    @DataSupplier
    public StreamEx<User> getUsers() {
        return getJsonRecords(User.class);
    }

    @DataSupplier
    public StreamEx<CrimeRecord> getCrimes() {
        return getCsvRecords(CrimeRecord.class).limit(1);
    }

В целом, потихоньку набираем обороты. Уже 44 звезды на GitHub. У кого есть идеи / пожелания относительно дальнейшего развития библиотеки, пишите в личку или создавайте issue на GitHub.

3 лайка

Версии 1.8.1 и 1.8.2 теперь полностью поддерживают Java 10 и Java 11 соответственно. Под поддержкой подразумевается полный рефакторинг к модульному формату.

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

ext {
    moduleName = 'your.module.name'
}
    
sourceCompatibility = JavaVersion.VERSION_11
    
repositories {
    maven {
        url "http://maven.springframework.org/milestone"
    }
    jcenter()
}
    
configurations {
    agent
}
    
dependencies {
    agent 'org.aspectj:aspectjweaver:1.9.2.RC'
    compile 'io.github.sskorol:test-data-supplier:1.8.2'
    testCompile 'org.testng:testng:6.14.3'
}
    
compileJava {
    inputs.property("moduleName", moduleName)
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath
        ]
        classpath = files()
    }
}
   
compileTestJava {
    inputs.property("moduleName", moduleName)
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath,
                '--patch-module', "$moduleName=" + files(sourceSets.test.java.srcDirs).asPath,
        ]
        classpath = files()
    }
}
   
test {
    useTestNG()
   
    inputs.property("moduleName", moduleName)
    doFirst {
        jvmArgs = [
                "-javaagent:${configurations.agent.singleFile}",
                '--module-path', classpath.asPath,
                '--add-modules', 'ALL-MODULE-PATH',
                '--add-opens', 'your.module.name/test.package.path=testng',
                '--add-opens', 'your.module.name/test.package.path=org.jooq.joor',
                '--patch-module', "$moduleName=" + files(sourceSets.test.java.outputDir).asPath
        ]
        classpath = files()
    }
}

Здесь важно открыть доступ к тестовым пакетам для TestNG / jOOR. Иначе тесты вы запускать не сможете из-за отсутствия привилегий для полноценной работы via Reflection API.

Ну и собственно module-info.java:

module your.module.name {
    requires io.github.sskorol.testdatasupplier;
    requires testng;
   
    // Optional
    provides io.github.sskorol.core.IAnnotationTransformerInterceptor
        with path.to.transformer.ImplementationClass;
   
    provides io.github.sskorol.core.DataSupplierInterceptor
        with path.to.interceptor.ImplementationClass;
}

Стоит так же обратить внимание на то, что AspectJ пока еще в RC состоянии для Java 11. Посему, в консоли будут появляться illegal access ворнинги. Но на работоспособность это никак не повлияет.

P.S. До Maven пока руки не дошли. Но на днях планирую выложить сэмпловые проекты.

1 лайк