
Surely everyone is familiar with such a concept as
test-driven development (TDD) . Along with it, there is also such a thing as
data-driven testing (DDT, no offense to Shevchuk) - a technique of writing tests in which data for tests are stored separately from the tests themselves. They can be stored in a database file, generated during the execution of the test. This is very convenient, since the same functionality is tested on different data sets, while adding, deleting or modifying this data is as simple as possible.
In a
previous article, I looked at the possibilities of
JUnit . There,
Parameterized and
Theories runners can serve as examples of this kind of approach, in both cases one test class can contain only one such parameterized test (in the case of
Parameterized, several, but they all will use the same data).
In this article I will focus on the
TestNG test framework. Many have already heard this name, and having gone to it, they hardly wish to return to JUnit (although this is only a guess).
Main features
So what is there? As in JUnit 4, tests are described using annotations, also tests written in JUnit 3 are supported. It is possible to use a doclet instead of annotations.
')
To begin, consider the test hierarchy. All tests belong to a sequence of tests (suite), include a number of classes, each of which may consist of several test methods. In this case, classes and test methods may belong to a particular group. It clearly looks like this:
+- suite/ +- test0/ | +- class0/ | | +- method0(integration group)/ | | +- method1(functional group)/ | | +- method2/ | +- class1 | +- method3(optional group)/ +- test1/ +- class3(optional group, integration group)/ +- method4/
Each member of this hierarchy may have
before and
after configurators. It all starts in this order:
+- before suite/ +- before group/ +- before test/ +- before class/ +- before method/ +- test/ +- after method/ ... +- after class/ ... +- after test/ ... +- after group/ ... +- after suite/
Now more about the tests themselves. Consider an example. The utility for working with locales, can parse from a string, and also search for candidates (
en_US -> en_US, en, root ):
public abstract class LocaleUtils { 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; } }
Let's write a JUnit-style test for it (this example should not be considered as a guide to writing tests for 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); } } }
What is there?
- As already mentioned in the previous article, I prefer to inherit the test class from Assert , this can be replaced with a static import, or using the class directly ( Assert.assertEquals (...) ). In a real system, it is most convenient to inherit a test from a base class, which in turn inherits from Assert , this makes it possible to override or add the necessary methods. Attention: in contrast to the same class in JUnit, here the actual value is transferred to all methods first, the second expected (in JUnit the opposite).
- Annotations @BeforeSuite , @AfterSuite denote methods that are executed once before / after the execution of all tests. Here it is convenient to place any heavy settings common to all tests, for example, here you can create a pool of database connections.
- Annotations @BeforeTest , @AfterTest denote methods that are executed once before / after the test execution (one that includes test classes, not to be confused with test methods). Here you can store the settings of a group of interrelated services, or a single service, if it is tested by several test classes.
- The @BeforeClass , @AfterClass annotations denote methods that are executed once before / after the execution of all tests in a class, are identical to the previous ones, but are applicable to test classes. Most applicable for testing a particular service that does not change its state as a result of the test.
- Annotations @BeforeMethod , @AfterMethod denote methods that are executed every time before / after the execution of the test method. Here it is convenient to store settings for a specific bean or service, if it does not change its state as a result of the test.
- Annotations @BeforeGroups , @AfterGroups denotes methods that run before / after the first / last test belonging to specified groups.
- Annotation @Test refers to the tests themselves. Here are the checks. Also applicable to classes
All of these annotations have the following parameters:
- enabled - can be temporarily disabled by setting the value to false
- groups - indicates which groups will be executed for
- inheritGroups - if true (and by default exactly so), the method will inherit the groups from the test class
- timeOut - the time after which the method will “crash” and pull all tests dependent on it
- description - the name used in the report
- dependsOnMethods - the methods on which it depends, they will be executed first, and then this method
- dependsOnGroups - groups on which depends
- alwaysRun - if set to true, it will always be called regardless of which groups it belongs to, does not apply to @BeforeGroups , @AfterGroups
Apparently from an example the test practically differs in nothing from the same test on JUnit. If there is no difference, then why use TestNG?
Parameterized Tests
We write the same test in another way:
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); } }
Simpler? Of course, data is stored separately from the test itself. Conveniently? Of course, you can add tests by adding just a line to the parseLocaleData method.
So how does it work?
- We declare a test method with all the parameters it needs, such as input and expected data. In our case, this is the string that needs to be parsed into the locale and the resulting locale.
- We announce the date of the provider, the data warehouse for the test. This is usually a method that returns Object [] [] or Iterator <Object []> , which contains a list of parameters for a specific test, for example {"en_US", Locale.US} . This method should be messed up using @DataProvider , in the test itself it is declared using the dataProvider parameter in the @Test annotation. You can also specify a name (the name parameter), if you do not specify the method name as the name.
One more example, now we will spread the data and logic of the test into different classes:
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 {
In this case, the
dataProviderClass and
dataProvider parameters are specified. The method that returns the test data must be
static .
In addition to the above, there is another way to parameterize tests. The required method is annotated using
@Parameters , where the names of all the necessary parameters are indicated. Some of the parameters can be annotated with the help of
@Optional with the indication of the default value (if not specified, the default values ​​for the primitives will be used, or null for all other types). Parameter values ​​are stored in the TestNG configuration (which will be discussed later). Example:
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) {
In this case, the
setUpDataSource method will accept database connection settings as parameters, with the
username and
password parameters optional, with the default values ​​specified. It is very convenient to use with data common to all tests (or almost all), for example, as in the example of setting up a database connection.
Well, in conclusion, I should say a few words about the factories that allow you to create tests dynamically. As well as the tests themselves, they can be parameterized using
@DataProvider or
@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);
Option with
@Parameters :
public class FactoryTest { @Parameters("table") @Factory public Object[] createParameterizedTest(@Optional("SOME_TABLE") String table) { return new Object[] { new GenericTableTest(table) }; } }
Multithreading
Need to check how an application behaves in a multithreaded environment? You can make tests run from multiple threads simultaneously:
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 determines the maximum number of threads used for tests.
- singleThreaded if set to true all tests will run in the same thread.
- InvocationCount determines the number of test runs.
- InvocationTimeOut determines the total time of all test runs, after which the test is considered failed.
The first test will occasionally fail with
ConcurrentModificationException , since it will run from different threads, the second - not, since all the tests will be run sequentially from one thread.
You can also set the
parallel parameter of the provider’s date to true, then the tests for each data set will be run in parallel, in a separate thread:
public class ConcurrencyTest extends Assert {
This test will display something like:
#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
Without this parameter there will be something like:
#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
Additional features
In addition to all of the above, there are other options, for example, to check for exceptions (it is very convenient to use for tests on incorrect data):
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 sets the options for expected exceptions; if they are not thrown, the test is considered failed.
- The expectedExceptionsMessageRegExp is the same as the previous parameter, but specifies regexp for the error message.
One more example:
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 defines the priority of the test inside the class; the smaller, the earlier it will be executed.
This example will be successful, since the method
first will be executed
first , then
second . If you change the priority of
first to 2, the test fails.
Similar behavior will be observed also if you specify dependencies for the test, for example, add to our test:
public class PrioritiesTest extends Assert {
This is usually convenient when one test depends on another, for example,
Utility1 uses
Utility0 , if
Utility0 does not work correctly, then there is no point in testing
Utility1 . On the other hand, dependencies are also conveniently used in
@Before ,
@After methods, especially to associate the base test with the inheritance, and sometimes it is necessary to make sure that even if method
A falls and method
B depends on it, method
B is still called. In this case, set the parameter
alwaysRun to true.
Dependency injection
I want to please fans of the framework from the "corporation of good»
Guice . TestNG has built-in support for the latter. It looks like this:
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!!!"); } }
All that is needed is to enchant the desired class using
@Guice and specify all the necessary guice modules in the modules parameter. Later in the test class, you can already use dependency injection using
@Inject .
I will also add that fans of other similar frameworks should not be upset, as they usually have their own support for TestNG, for example, Spring.
Expansion of functionality
Expansion of functionality can be implemented using the mechanism of listeners. The following listener types are supported:
- IAnnotationTransformer, IAnnotationTransformer2 - allow you to override test settings, for example, the number of threads to start a test, the timeout, the expected exception:
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}); } } }
This example would expect to throw an exception from test methods starting with expect. - IHookable - allows you to override the test method or, if possible, skip, an example with JAAS is provided in the tutorial on TestNG.
- IInvokedMethodListener, IInvokedMethodListener2 - similar to the previous listener, but executes the code before and after the test method execution
- IMethodInterceptor - allows you to change the test run order (applies only to tests that are independent of other tests). There is a good example in the tutorial.
- IReporter - allows you to extend the functionality that is performed after the execution of all tests, usually this functionality is associated with the generation of error reports, etc. This way you can implement your own reporting engine.
- ITestListener - a listener that can handle most events from a test method, for example, start, finish, success, failure
- ISuiteListener - similar to the previous one, but for suites, it receives only the start and finish events
One of the examples of interesting use of the mechanism of listeners can be read
here .
Configuration
We now turn to the test configuration. The easiest way to run tests looks like this:
final TestNG testNG = new TestNG(true); testNG.setTestClasses(new Class[] {SuperMegaTest.class}); testNG.setExcludedGroups("optional"); testNG.run();
But most often for start of tests XML or YAML a configuration is used. The XML configuration looks like this:
<!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>
Similar YAML configuration:
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.*
Then to run the tests you will need to do the following:
final TestNG testNG = new TestNG(true);
Although, probably, the program launch of tests is unnecessary, since for this you can use the IDE features.
Let's go back to the configuration itself. At the very top level, the test sequence (suite) is configured. It can take the following parameters:
- name - the name used in the report
- thread-count - the number of threads used to run tests
- data-provider-thread-count - the number of threads used to transfer data from data providers to the tests themselves for parallel data providers ( @DataProvider (parallel = true) )
- parallel - can have the following values:
- methods - test methods will be run in different threads, you need to be careful if there are dependencies between methods
- classes - all methods of the same class in the same thread, but different classes in different threads
- tests - all methods of the same test in the same thread, different tests in different threads
- time-out - the time after which the test will be considered failed, the same as in the annotation, but applies to all test methods
- junit - JUnit 3 tests
- annotations - if javadoc, then the doclet will be used for configuration
Also can be customized:
- parameter - the parameters used in @Parameters
- packages - packages where to look for test classes
- listeners are listeners, with their help you can extend the functionality of TestNG, a couple of words have already been said about them
- method-selectors - selectors for tests, must implement the IMethodSelector interface
- suite-files - you can include other configuration files
The suites, in turn, can include tests with practically the same settings as for the suites (
name, thread-count, parallel, time-out, junit, annotations ,
parameter, packages, method-selectors tags). The tests also have some peculiar settings, for example, the groups to be run:
<test name="Default tests" verbose="1" annotations="JDK" thread-count="10" parallel="classes"> <groups> <run> <exclude name="integration"/> </run> </groups> </test>
In this example, the test will include only tests not related to the integration group.
Other tests may include test classes, which in turn may include / exclude test methods.
<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>
Conclusion
This is not all that can be said about this wonderful framework; it is very difficult to cover everything in one article. But I tried to reveal the main points of its use. Here, of course, it was possible to tell a little more about the mechanism of the listeners, about integration with other frameworks, about the configuration of tests, but then the article would turn into a book, and I don’t know everything. But I hope this article was useful to someone and what was said will encourage some readers to use TestNG, or at least on the DDT technique of writing tests.
Examples can be found
here , various articles
here .
Literature