Best Practices And What Should Be Avoided in Test Automation
Use Small, Atomic, Autonomous Tests
When a test fails, the most important thing is knowing what went wrong so you can easily come up with a fix. The best way to know what went wrong is to keep three words in mind when designing your tests: Small, Atomic, and Autonomous.
Small
Small refers to the idea that your tests should be short and succinct. If you have a test suite of 100 tests running concurrently on 100 VMs, then the time it will take to run the entire suite will be determined by the longest/slowest test case. Keeping your tests small ensures that your suite will run efficiently and provide you with results faster.
Atomic
An atomic test is one that focuses on testing a single feature, and which makes clear exactly what it is that you're testing. If the test fails, then you should also have a very clear idea of what needs to be fixed.
Autonomous
An autonomous test is one that runs completely independently of other tests, and is not dependent on the results of one test to run successfully. In addition, an autonomous test should use its own data to test against, and not create potential conflicts with other tests over the same data.

Tests should be easy to read and write
- meaningful method names - try to pick a pattern for that
- AAA (Arrange Act Assert) pattern for writing tests (similar to Gherkin format - Given, When, Then)
AAA pattern
- Arrange all necessary preconditions and inputs.
- Act on the object or method under test.
- Assert that the expected results have occurred.

- Clearly separates what is being tested from the setup and verification steps.
- Makes some "test smells" more obvious:
- Assertions intermixed with "Act" code.
- Test methods that try to test too many different things at once.
Benefits
Avoid External Test Dependencies
-
use Setup and Teardown methods (test fixtures)


Text
Problem - You want to avoid duplicated code when several tests share the same initialization and cleanup code.
-
When a setUp() method is defined, the test runner will run that method prior to each test. Likewise, if a tearDown() method is defined, the test runner will invoke that method after each test.
-
"Prerequisite" tasks that need to be taken care of before your test runs, you should include a setup section in your script that executes them before the actual testing begins. For example, you may need to to log in to the application, or dismiss an introductory dialog that pops up before getting into the application functionality that you want to test.
- "Post requisite" tasks that need to occur, like closing the browser, logging out, or terminating the remote session, you should have a teardown section that takes care of them for you.
-
Benefit - avoids code duplication, makes code more readable and maintainable
Setup And Teardown Methods
| Feature | JUnit | TestNg |
|---|---|---|
| run before each @Test method | @Before | @BeforeMethod |
| run after each @Test method | @After | @AfterMethod |
| run before the first test method in the current class is invoked | @BeforeClass | @BeforeClass |
| run after all the test methods in the current class have been run | @AfterClass | @AfterClass |
| run before all tests in this suite have run | - | @BeforeSuite |
| run after all tests in this suite have run | - | @AfterSuite |
More General Fixtures Can Be Put to TestBase class

...this way test class only has testing code without any preparation and teardown code
...and Test class should extend the BaseTest class...

Don't Hard Code Dependencies on External Accounts or Data
- Development and testing environments can change significantly in the time between the writing of you test scripts and when they run, especially if you have a standard set of tests that you run as part of your overall testing cycle.
- For this reason, you should avoid building into your scripts any hard coded dependencies on specific accounts or data.
- Instead, use API requests to dynamically provide the external inputs you need for your tests.

Incorrect Approach

Correct Approach
- In previous example, credentials are taken from config file (simple external file) by ConfigReader class. In order to achieve this, you only need to parse the file (basically, read it programmatically)


Use DDT
- Data-driven testing is creation of test scripts where test data and/or output values are read from data files instead of using the same hard-coded values each time the test runs
- Allows you to use a once-written test to test various conditions with different sets of test data. These sets are usually stored separately from scripts in external files or downloaded from the database.
DDT in JUnit

DDT in TestNg

DDT in Cucumber

Remember
- Reuse variables!
- If test is should be written with different values (boundary, equilance partitioning), don`t copy/paste the test, take advantage of DDT technique instead
- If you have multiple test environments with different test data, recommended to create config file/profile per each environment
Avoid Dependencies between Tests
Dependencies between tests prevent tests from being able to run in parallel. And running tests in parallel is by far the best way to speed up the execution of your entire test suite. It's much easier to add a virtual machine than to try to figure out how to squeeze out another second of performance from a single test.
Incorrect Approach - testUserOnlyFunctionality() test depends on testLogin() test

Correct Approach - testUserOnlyFunctionality() test does NOT depend on testLogin() test

...but if you have such situation:

...then try to setup a state via API (firstly, with available external API)
- REST, SOAP, any external API available
-
DB
- NOTE - Try to avoid accessing the database of the service in order to create the test data. This leads to high coupling. If the schema or database technology changes, your tests will break. But sometimes it’s inevitable to access the database. Don’t be dogmatic. But be aware of the high coupling.
Parallelize Your Tests
- Each test data should be unique across the test suite
- Each test deletes only data that it created
Add Screenshot taking capability for better debugging

Use Explicit Waits instead of Thread Sleep and Implicit waiting!!!
NOTE - If you still want to use implicit waiting, then do not mix implicit and explicit waits. Doing so can cause unpredictable wait times. For example setting an implicit wait of 10 seconds and an explicit wait of 15 seconds, could cause a timeout to occur after 20 seconds.
-
Explicit wait:
-
documented and defined behaviour.
-
runs in the local part of selenium (in the language of your code).
-
works on any condition you can think of.
-
returns either success or timeout error.
-
can define absence of element as success condition.
-
can customize delay between retries and exceptions to ignore
-
-
Implicit wait:
-
undocumented and practically undefined behaviour.
-
runs in the remote part of selenium (the part controlling the browser).
-
only works on find element(s) methods.
-
returns either element found or (after timeout) not found.
-
if checking for absence of element must always wait until timeout.
-
cannot be customized other than global timeout.
-
TIP: put Explicit Waiting in every Page Object constructor

Don't Use Brittle Locators
-
Instead of: By.xpath("//tr/td[3]/div/div[text()='Some Text']"
-
Just use : By.xpath("//*[text()='Some Text']"
Example
- Agree with devs about locators if possible. Particularly, find out which locators cannot be used and which are good candidates. Best case scenario - they should provide unique locators for visible on the page HTML elements. Unique locators might be an "id", a new unique class, new attribute (e.g. propid="unique_id")
- Check for dynamically generated locators, since they might change with time. Again, check with devs if possible
(E.g. id="td_1")
Some Tips On Locators
Use Page Object and PageFactory pattern
Benefits of using Page Object pattern
-
Clear separation between test code and navigation code in code base.
-
Makes tests more readable
-
Ease of maintenance
REMEMBER!
- Page Object should expose the "interface" to the Page (contains behavior - methods to interact with the page)

- Page Object returns some info about the page (e.g. new page object, text from page, a boolean result for some check, etc. - never a WebElement (but might be some exceptions :) )).
- This means that WebElement should be private and interface to them should be provided via public methods.

- Verifies page ready state as part of initialization with a found Element

- Page Object should not contain assertions, only in test!
- Page Object method should be named as from Business logic perspective - avoid naming methods as "clickOnSaveButton()"

- It`s prefered to include the type of the element to WebElement name so that it`s instantly clear what it is and how to interact with it from the test. E.g.:
- "searchCriteriaInput" or "inptSearchCriteria",
- "timePeriodDropdown", "drpdwnTimePeriod"
- In addition, WebElement names should not contain verb in them (e.g. - "
goToElectronicsLink")
- Repeating actions in multiple Page Objects should be put into separate helper class.
- Make method static so that there will be no need to initialize an instance


- Recommended to return other Page Objects in a method which end up going to another page, but downside is that it may be necessary to model (for example) both a successful and unsuccessful login, or a click could have a different result depending on the state of the app. When this happens, it is common to have multiple methods on the Page Object


- If a method stays on the same page, then return the same Page Object ("return this"). This will make your tests more "fluent". More on this https://en.wikipedia.org/wiki/Fluent_interface


PageFactory Pattern

- PageFactory.initElements() method usually is put into corresponding Page Object constructor
Test Automation Best Practices
By Ilja Pavlovs
Test Automation Best Practices
- 351