-
Notifications
You must be signed in to change notification settings - Fork 44
Requirements
- What are requirements and how to use them
- Implementing a new requirement
- Requirements under the hood
Requirements are one of the most important part of RedDeer. Usually, when you want to execute tests, you need an environment in proper state, you need something to be set etc. Basically, you require a specific state, specific actions to be taken before your tests execute. And for that there are requirements.
Requirements are annotation driven setup of test environment. It is executed once for each class before @BeforeClass
method.
There are 2 basic types of requirements:
Basic Requirement implements interface org.eclipse.reddeer.junit.requirement.Requirement<T extends Annotation>
. There are several default requirements of such type, e.g. AutoBuildRequirement (to turn on/off auto building), CleanWorkspaceRequirement (remove all projects from workspace), CloseAllEditorsRequirement and many more
We want to make sure that workspace does not contain any projects so we use CleanWorkspace requirement.
@CleanWorkspace
public class MyUltimateTest {
...
@Test
public void myTest(){
//test something
}
...
}
Configurable Requirement implements interface org.eclipse.reddeer.junit.requirement.ConfigurableRequirement<T extends RequirementConfiguration, K extends Annotation>
. Such requirements use additional configuration obtained from a configuration file. There are several default configurable requirements such as ServerRequirement, DatabaseRequirement, PropertyRequirement and many others.
We want to make sure that JRE is defined.
@JRE
public class MyUltimateTest {
...
@Test
public void myTest(){
//test something
}
...
}
Since JRE is a complex requirement it requires a configuration file. In case of JRE it will look like this (it is a json file)
{
"org.eclipse.reddeer.requirements.jre.JRERequirement.JRE": [ //FQN of JRE requirement annotation
{
//requirement properties
"name": "jre-name",
"version": "1.7",
"path": "/path/to/jre/home"
}
]
}
When we run our test we have to specify -Drd.config=/path/to/config/json/file
We can also define more JREs in configuration file
{
"org.eclipse.reddeer.requirements.jre.JRERequirement.JRE": [
{
"name": "jre-name1",
"version": "1.7",
"path": "/path/to/jre17/home"
}
],
"org.eclipse.reddeer.requirements.jre.JRERequirement.JRE": [
{
"name": "jre-name2",
"version": "1.8",
"path": "/path/to/jre18/home"
}
]
}
In this case, our test will run twice - for both JREs defined.
Usually there is a configuration file with plenty of configurations. Some time we don't want to run a specific test class with all of those configuration. In such cases we can restrict which configurations should be used for test execution by implementing a public static method returning a requirement matcher or collection of requirement matchers and annotated with @RequirementRestriction
. Then, in the moment of building test suites, only configurations matching a returned matcher will be used for a test class.
Our configuration file
{
"org.eclipse.reddeer.requirements.jre.JRERequirement.JRE": [
{
"name": "jre-name1",
"version": "1.7",
"path": "/path/to/jre17/home"
}
],
"org.eclipse.reddeer.requirements.jre.JRERequirement.JRE": [
{
"name": "jre-name2",
"version": "1.8",
"path": "/path/to/jre18/home"
}
]
}
Our test class
@JRE
public class MyUltimateTest {
...
@RequirementRestriction
public static RequirementMatcher getRestrictionMatcher() {
return new RequirementMatcher(JRE.class, "version", new VersionMatcher(">1.7"));
}
@Test
public void myTest(){
//test something
}
...
}
Our test will run only with JREs which version is >1.7.
We can also return Collection of RequirementMatcher-s as follows:
@JRE
public class MyUltimateTest {
...
@RequirementRestriction
public static Collection<RequirementMatcher> getRestrictionMatcher() {
return Arrays.asList(
new RequirementMatcher(JRE.class, "version", new VersionMatcher(">1.7")),
new RequirementMatcher(JRE.class, "name", new RegexMatcher("jre.*")));
}
@Test
public void myTest(){
//test something
}
...
}
Requirement matcher takes requirement annotation class, attribute name and a matcher or a string to match attribute. Attribute could be nested. For nested attributes there is used a dot notation - e.g. nesteConfig.anotherNestedConfig.version
One of the two most useful matchers to be used are RegexMatcher and VersionMatcher located in common plugin. But this does not restrict you to use only those 2 matchers. RequirementMatcher takes any matcher matching a string value.
We can inject requirement to specified field as follows:
@JRE
public class MyUltimateTest {
...
@InjectRequirement
JRERequirement jre;
@Test
public void myTest(){
//access JRE requirement configuration
JREConfiguration configuration = jre.getConfiguration();
//get JRE version
configuration.getVersion();
}
...
}
Requirements does not only modify you test environment but they change test suites for execution as well. For every set of configurable requirements there is a special suite constructed. For a class with 2 configurable requirements, each of them having 2 available configurations, there are 4 suites in total. It is possible to restrict a suitable configuration for a test class Let's have a look at example. We have a configuration file with 2 servers and 2 databases configuration in it, and we have following test classes
@Database
@ApacheTomcatServer
public class TestClass1 {...}
@Database
@ApacheTomcatServer
public class TestClass1 {...}
This will result into execution of 4 test suites with combinations of servers and databases (Cartesian product):
DB1-SRV1
TestClass1
TestClass2
DB1-SRV2
TestClass1
TestClass2
DB2-SRV1
TestClass1
TestClass2
DB2-SRV2
TestClass1
TestClass2
Have in mind that this is true only for configurable requirements. Other requirement implementing Requirement interface does not affect amount of test suites.
To successfully implement a new requirement, there is a list of tasks or more like requirement pattern to be done:
-
requirement annotation
- each requirement implementing a requirement interface must have an annotation for requirement declaration
- annotation have to be in the same class as a requirement and must be public (due to logic how requirements are obtained and processed)
- annotation can contain some elements... These elements could be perceived as static set up of a requirement
- annotation has to be annotated with
@Retention(RetentionPolicy.RUNTIME)
and@Target(ElementType.TYPE)
(due to logic how requirements are obtained and processed)
- implementation of interface methods - all methods from a requirement interface should be implemented, if possible
There are additional steps for configurable requirements:
- requirement has to be registered via extension point
org.eclipse.reddeer.juni.requirement
<plugin>
<extension point="org.eclipse.reddeer.junit.requirement">
<requirement class="FQN of your requirement"/>
</extension>
</plugin>
- RequirementConfiguration has to be a POJO object with setters and getters for successful deserialization from json configuration file.
This section contains several more advanced topics.
Configurations from a configuration file are built for a test run. This happens when a suite is built and it is one time action. Keep this in mind when working on tests/extending functionality of builders, runners, configuration pool and configuration readers in RedDeer JUnit plugin. There is a static method RequirementConfigurationPool#destroyPool
which could come handy.
For now, there is only JSON configuration reader. But that's not final. You can implement your own reader by contributing to RedDeer and implementing interface org.eclipse.reddeer.junit.internal.configuration.reader.ConfigurationReader
in RedDeer JUnit plugin and adding few lines of code to RequirementConfigurationPool#initRequirementConfigurationPool. If you decide to go for a dynamically constructed requirements configurations from a configuration file, check JSONConfigurationReader#loadClass(String className)
method.