A faster way to clean all inputs in an HTML form
December 19, 2020Add more information to your Allure Report using Java
January 23, 2021This post belongs to the How to create Lean Test Automation Architecture for Web using Java series. If you didn’t see the previous posts, please check them out!
Introduction
A good practice when we are creating an automated test framework using Java is the creation of properties files to add any configuration that can be changed either manually in the file or when you are running the tests using a CI/CD tool.
It’s commonly used to change the URL, main users, timeout, or whatever data you need to change frequently as a configuration.
The main problem is to manage one or more properties files we might have in our project. This article will show you how to easily manage it with Owner Java library.
The most used way to manage properties files
The property file
Let’s imagine we will use a property file to manage the different data and/or configurations for a web test. We have this file as an example:
# target execution: local or grid
target = local
# initial URL
url.base = https://eliasnogueira.com/external/selenium-java-architecture/
# global test timeout
timeout = 3
# headless mode only for chrome or firefox and local execution
headless = false
The old and plain way to read properties file
There are a lot of ways we can read a property file using Java. This example is a simple one using the Singleton design pattern:
public class ReadProperties {
private static final Logger LOG = LoggerFactory.getLogger(ReadProperties.class);
private static ReadProperties instance = null;
private Properties properties = null;
private ReadProperties() {
properties = new Properties();
try {
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("general.properties");
properties.load(inputStream);
} catch (IOException e) {
LOG.error(e.getMessage());
}
}
public static synchronized ReadProperties getInstance() {
if (instance == null) {
instance = new ReadProperties();
}
return instance;
}
public String getValue(String key) {
return this.properties.getProperty(key, String.format("The key %s does not exists!", key));
}
}
Now we can get a value from the property file as:
String url = ReadProperties.getInstance().getValue("url.base");
Possible problems
- Remove the singleton pattern to be able to get the property value from different files
- Manage different files explicitly in your code
- An extra layer (code) to manage the properties into a model to enable readability
The Owner way
Owner is a library that makes easy the properties file management.
You can easily read properties values by:
- creating a class associating each property name
- using it first to create the property object and after reading its value
Import the library
You can use Owner import its library. Please double-check the latest version.
<dependencies>
<dependency>
<groupId>org.aeonbits.owner</groupId>
<artifactId>owner</artifactId>
<version>1.0.8</version>
</dependency>
</dependencies>
How to define the properties class
We will model a class containing the properties as an interface and extend the Owner Config
class. We also need to use an annotation to define the configuration file.
import org.aeonbits.owner.Config;
@Config.Sources({"classpath:general.properties"})
public interface GeneralConfig extends Config {
String target();
@Config.Key("url.base")
String url();
int timeout();
Boolean headless();
}
- On line 3, we are telling Owner to load the
general.properties
file that is placed in the classpath
- On line 4, we define the model as an Interface (it’s required by Owner) and extend it to use the
Config
class (this class does most of the Owner’s magic).
- From lines 6 to 14, we are defining the properties with the same name we have in the properties file.
Owner will do the trick automatically if you have the property’s names as the same defined in the properties file. Example: the target()
attribute in the class has the same name as the target property in the general.properties file.
If you have a composite name, as we have for url.base
you can use the @Config.Key
annotation to associate the property in the general.properties
file into the class.
Another benefit is that you can use the correct object types. The headless attribute is a Boolean
, so Owner will automatically do the type inference.
How to read the values from the properties file
We need to create the model and use it! Easy, right?
public class OwnerUsageExample {
public void example() {
GeneralConfig generalConfig = ConfigFactory.create(GeneralConfig.class);
String target = generalConfig.target();
String url = generalConfig.url();
int timeout = generalConfig.timeout();
Boolean headlessMode = generalConfig.headless();
}
}
- On line 4 you can see we have the
GeneralConfig
class and attribute receiving theConfigFactory.create(GeneralConfig.class)
. This line will tell Owner to load thegeneral.properties
file and associate all the properties and values to theGeneralConfig
class.
- In lines 6 to 9, we are using the attribute
generalConfig
to read the properties values. Each method will return the corresponding property value.
Dealing with constraints
Multiples properties file
Do you have multiple properties files to handle? No problem!
You can create one model for each property or you can merge them all in one. The first option you already know after you got here and I will explain the merge approach.
Let’s say you have, in addition to the general.properties
file, a new one named grid.properties
.
grid.url = localhost
grid.port = 4444
Now how we can merge both?
import org.aeonbits.owner.Config;
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({
"classpath:general.properties",
"classpath:grid.properties"
})
public interface GeneralConfig extends Config {
String target();
@Config.Key("url.base")
String url();
int timeout();
Boolean headless();
@Config.Key("grid.url")
String gridUrl();
@Config.Key("grid.port")
String gridPort();
}
- On line 3 we are using the
@Config.LoadPolicy(Config.LoadType.MERGE)
annotation to tell Owner to merge files.
- On lines 5 and 6 we have the two properties files declared
- On lines 19 to 23 we have the two new properties mapped
So now you can have only one model to use more than one properties file.
Read the System Properties
Probably you will use a CI/CD tool to configure your application, run tests, and do other magic activities. This is managed, in Java, by the System properties which we can set through the code or using command line parameters. For properties file, we use -DpropertyName=propertyValue
.
Now we need to tell Owner to look at the System properties.
@Config.LoadPolicy(Config.LoadType.MERGE)
@Config.Sources({
"system:properties",
"classpath:general.properties",
"classpath:grid.properties"
})
public interface GeneralConfig extends Config {
}
- On line 3 we added the
"system.properties"
sources, so Owner can first look at it and set the property value
Create a singleton instance
We were creating the model object to access the properties values using GeneralConfig generalConfig = ConfigFactory.create(GeneralConfig.class);
You would add this line in every class you need to access the properties values, right? But you will end up creating multiple instances of the same object. In this case, it should be a singleton.
Owner provides a similar class as ConfigFactory
but instead of creating a new object every time we need to access the properties values, the ConfigCache
class can be used.
public class OwnerSingletonUsageExample {
public void example() {
GeneralConfig generalConfig = ConfigCache.getOrCreate(GeneralConfig.class);
// getting values ignored
}
}
Using ConfigCache.getOrCreate
Owner will create a single instance of the model. You can learn more about this approach here.
Real examples
Would you like to see real examples? I have examples for Web, API, and Mobile projects.
Automated Web Test
In my selenium-java-lean-test-architecture project you can found:
- The configuration files
- The model for the properties files
- The Manager for the model of the properties files as a static class
- The general code usage
- In the factory class to get the faker locale
- In the driver factory class to get the target environment
- In the remote driver to get the grid URL and port
- In the local driver to get the headless mode
- In the abstract page object class to get the global timeout
Automated API Test
In my restassured-complete-basic-example project you can find the following:
- The configuration file
- The model for the properties files
- The Manager for the model of the properties files as a static class
- The general code usage
- In the base test class
- In the custom message format class
- In the initial state specs
10 Comments
Can we set a new property in the properties file?
Hi Kalyan,
Yes! You can add the new property in your properties file, but you also need to add the same in the configuration class.
If the name is simples, like
newproperty
Owner will automatically bind the sameString newproperty
attribute in the class.If the name has dots, for example, or you need to give the variable in the configuration class another name, you must use
@Config.Key("property-name")
in the top of the attribute.Regards!
Can we write also to properties file?
Unfortunately, no.
To write and save a value back you need to use the Java Properties class.
@ELIAS. Thanks for sharing details about owner library. Based on understanding, I have few comments and questions.
This looks good for demo projects but I see big issue for enterprise framework.
Let say we created automation framework as maven module. Different project teams to leverage this framework, generally teams will add framework module as dependency in their pom file.
Here each team will have different keys and we cannot define with annotation @keys as we will have not have control.
Is there a way we can handle with this library?
Thanks,
Ravi
Hi Ravi,
Unfortunately, you cannot achieve this with this library.
I have the same problem with multi-module projects, where we are using the Apache Commons Configuration to manage it for us, where we can have global environment variables defined by the base library and internal ones for some modules.
Hi Elias, thanks for share you knowledge.
I am creating a framework with selenium-serenity bb- cucumber for mobile testing.
I need to know the correct way to read three properties files in run time such as android.properties, ios.properties and local.properties.
I have a test case in a feature with 3 scenarios, first scenario must read android.properties, the second scenario must read ios.properties and the third must read local.properties.
How can I get this?
Hi!
You can achieve this by using:
* Owner
* Apache commons
If the properties inside the properties file are different you can use the mentioned libraries in the following approach:
With Owner, you can load set multiple properties to read where it’s influenced by the Loading Strategy
http://owner.aeonbits.org/docs/loading-strategies/
Using Apache Commons you need to work with the Composite Configuration to load more than 1 properties file.
If they have the same value I would recommend you create an approach using Apache Commons, where you need to define the file to load the values first.
Great article. Just learned about the owner. I Have a question, How can we pass values dynamically for the below scenario?
@DefaultValues(“{path1}/test/{path2}”)
String testPath();
static Sting path1 = “hello”;
static Sting path1 = “owner”;
Thank you for the feedback!
I believe you can use the Mutable interface to change the value in runtime.
https://matteobaccan.github.io/owner/docs/accessible-mutable/
I think not in the composition you are adding in the
@DefaultValues()
, but using directly the configuration object to set what’s dynamic date for you.