Fix Flaky UI Tests


An effective test automation strategy requires three different levels of automated tests, as shown in the figure below, which depicts the test automation pyramid defined by Mike Cohn in his book “Succeeding with Agile”



Automated user interface (UI) testing is placed at the top of the test automation pyramid.
The target is to test the whole system, end-to-end, via its user interface, from the customer’s point of view. UI tests simulate user interactions, checking the entire application behaves as expected.
UI testing is the most comprehensive level of testing; these tests give you the biggest confidence when you need to decide if your software is working or not.
Neither integration nor unit can give such confidence.
UI tests go end-to-end through the entire system; this is why we all love them. Moreover, end-to-end tests that focus on real user scenarios sound like a great idea!
The main drawbacks of this type of test are:
• slow
• fragile
• flaky / non-deterministic

Slow

It’s OK for those tests to run pretty slowly (especially compared to unit tests) because they go end-to-end through more layers of the system.

Fragile

UI Tests tend to be fragile. Changing even one little thing in the UI may break your tests.

Writing UI tests not so closely tied to the user interface, and decoupling test data from test scripts (logic), using a data-driven approach, allows creating more robust UI tests.

Flaky

UI / end-to-end tests were flaky at times. Flaky means they don’t always run reliably: sometimes they pass, sometimes they fail (without any code changes).

The main cause of flakiness is often an “inconsistent application state“.

When your application state is not consistent between test runs, e.g. due to application responsiveness (timing), you can notice that test actions or assertion statements fail randomly. The fix for this is to construct tests so that you wait for the application to be in a consistent state before performing a test action or asserting.

Sometimes you see that you cannot run a test in a test suite and it only passes when it is run in isolation. Or you can run a test in a test suite, but you cannot run it in a subset of the whole test suite. In this case, the cheapest fix is to carefully select the test order in-suite. The most reliable fix is to make sure that your application is in a consistent state every time before the test starts.

Using Maveryx + Awaitility in UI testing, you can check your application state is consistent in different scenarios.

Here are some practical examples.

The code is well commented on so you can understand what each line is doing.

Consider the case of a CRM like Siebel.



  1. Waiting for the UI object to test is present.

Sometimes when moving from one page to another, or dynamically into the same page, you have to wait for a UI element to be present on the page before acting on it. The waiting time could be varied, sometimes very short and other times longer.

You could simply suspend (Thread.sleep) the current test execution for the specified amount of time:

             //the UI object to test
		GuiText clientID = new GuiText("ID Cliente");

		//wait for 5 seconds
		Thread.sleep(5000); 
		
                //type text into the text field
		clientID.setText("John_Smith_072921");

		//check the value into the text field
		assertEquals("John_Smith_072921", clientID.getText());

Avoid (!!!) “waiting” between steps for fixed periods of time (it increases testing time); write instead your tests to poll (waitForObject) your application continuously to ensure it is in the appropriate state before asserting on the step criteria.

             //the UI object to test
		GuiText clientID = new GuiText("ID Cliente");

		//wait up to 5 seconds the text field is present in the UI.
		clientID.waitForObject(5, 1);
		
		//type text into the text field 
		clientID.setText("John_Smith_072921");

		//check the value into the text field
		assertEquals("John_Smith_072921", clientID.getText());

If the object will become not available within the given time, an ObjectNotFoundException will be returned.

  1. Waiting for control is enabled. 

Sometimes you have to wait for a UI element to become enabled before acting on it. Also in this case, e.g. in dynamic systems, the waiting time could be varied, sometimes very short and other times longer.

If you write:

                //the UI object to test
		GuiText clientID = new GuiText("ID Cliente");
		
		//type text into the text field
		clientID.setText("John_Smith_072921");
		
		//check the value into the text field
		assertEquals("John_Smith_072921", clientID.getText());

This test can pass, or  fail.

It will pass if the text field will become rapidly enabled. Otherwise, the type action won’t happen but the code will continue to execute and the subsequent assertion will fail with a false positive.

You should rather code your test in this way:

                //the UI object to test
		GuiText clientID = new GuiText("ID Cliente");
		
		//wait up to 5 seconds the text field is enabled, before typing some text  
		await().atMost(5, SECONDS).until(() -> clientID.isEnabled());
		
		//type text into the text field
		clientID.setText("John_Smith_072921");
		
		//check the value into the text field
		assertEquals("John_Smith_072921", clientID.getText());

If the await() condition is not satisfied the following exception is returned:

org.awaitility.core.ConditionTimeoutException: Condition with lambda expression was not fulfilled within 5 seconds.

Through this approach, if the test will fail you should investigate your application under test.

  1. Waiting for a field to be filled.

Similarly to the previous case, sometimes you have to wait for a UI element to be filled before acting or asserting on it. Also, in this case, the waiting time could be varied, sometimes very short and other times longer.

If you write:

                //the UI object to test
		GuiText clientID = new GuiText("ID Cliente");

		//check the value in the text field
		assertEquals("John_Smith_072921", clientID.getText());

This may or may not result in a passing test.

It will pass if the text field will be rapidly filled, otherwise, it will fail (false positive).

A more reliable approach is:

                //the UI object to test
		GuiText clientID = new GuiText();

                //wait up to 5 seconds for the text field is not blank, before asserting on its content
		await().atMost(5, SECONDS).until(() -> clientID.getText() != "");

		//check the value in the text field
		assertEquals("John_Smith_072921", clientID.getText());

Flakiness is one of the challenges of automated UI testing. When your tests are flaky, a test failure may or may not mean that there’s an issue in the application under Test. Do always a root cause analysis to understand why they’re flaky. Sometimes, this could lead to uncover a bug in your UI tests due to invalid assumptions about the state of the application, dependencies on the timing of the application, and more.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.