Loading...

Preventing flaky tests in your automation suite

 There are many ways of dealing with unstable tests. Some of them are very complex and can be called more like a process but some are are very simple to introduce. In this article I will focus on one very simple and chip idea to introduce - mainly if you using java and junit. The idea is in general to run newly created test several times (e.g. 50 times) before merging it to branch with tests run regularly.

To achieve that you need two things. Thirst of you have to create test category for marking test cases which need to be verified from stability point of view. I have described the way of creating test categories it in post How to categorise automation tests. Thus, create new empty interface e.g. StabilityCheckCategory and with use of this category create new maven profile like shown below:

<profile>
	<id>stability-check</id>
	<properties>
		<test.case.excluded.groups/>
		<test.case.groups>com.testcraftsmanship.model.configuration.StabilityCheckCategory</test.case.groups>
	</properties>
</profile>


The next thing to add is a new implementation of TesRule in which you will check if test is marked with StabilityCheckCategory annotation. If yes, then it should run the statement in the loop desired number of times (number of times kept in the variable Constants.NUMBER_OF_REPETITIONS_IN_STABILITY_CHECK). If the test case is not marked with the StabilityCheckCategory annotation, then test will be run with use of default Statement implementation. Below is the example implementation of that TestRule:

package com.testcraftsmanship.model.base;

import com.testcraftsmanship.model.configuration.Constants;
import com.testcraftsmanship.model.configuration.StabilityCheckCategory;
import org.junit.experimental.categories.Category;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.Optional;

import static com.testcraftsmanship.model.base.DescriptionDataExtractor.getCategory;
import static com.testcraftsmanship.model.base.DescriptionDataExtractor.getClassName;
import static com.testcraftsmanship.model.base.DescriptionDataExtractor.getMethodName;

public class RepeatRule implements TestRule {
    private static final Logger LOGGER = LoggerFactory.getLogger(RepeatRule.class);

    private static class RepeatStatement extends Statement {
        private final Statement statement;
        private final int repeat;

        RepeatStatement(Statement statement, int repeat) {
            this.statement = statement;
            this.repeat = repeat;
        }

        @Override
        public void evaluate() throws Throwable {
            for (int iteration = 0; iteration < repeat; iteration++) {
                LOGGER.info("Test case run number {} of {}", iteration + 1, repeat);
                statement.evaluate();
            }
        }
    }

    @Override
    public Statement apply(Statement statement, Description description) {
        Statement result = statement;
        Optional<Category> category = getCategory(description);
        if (isStabilityCheckCategory(category)) {
            LOGGER.info("Test case {} from class {} will be repeated {} times",
                    getMethodName(description),
                    getClassName(description),
                    Constants.NUMBER_OF_REPETITIONS_IN_STABILITY_CHECK);
            result = new RepeatStatement(statement,
                    Constants.NUMBER_OF_REPETITIONS_IN_STABILITY_CHECK);
        }
        return result;
    }

    private boolean isStabilityCheckCategory(Optional<Category> category) {
        return category.isPresent() &&
                Arrays.asList(category.get().value()).contains(StabilityCheckCategory.class);
    }
}


In the class above I have moved some functionality to the external class to make code more clean. Class DescriptionDataExtractor contains only methods which extracts class name with test case, test method name and test category from description object. Below is implementation of this class:

package com.testcraftsmanship.model.base;

import org.junit.experimental.categories.Category;
import org.junit.runner.Description;

import java.util.Optional;

class DescriptionDataExtractor {
    static String getClassName(Description description) {
        return description.getTestClass()
                .getName().split("\\.")[description
                    .getTestClass().getName().split("\\.").length - 1];
    }

    static String getMethodName(Description description) {
        return description.getMethodName().split("\\[")[0];
    }

    static Optional<Category> getCategory(Description description) {
        return Optional.ofNullable(description.getAnnotation(Category.class));
    }
}


The last thing you have to do is adding the newly created rule to our test classes:

@Rule
public RepeatRule repeatRule = new RepeatRule();


I suggest to create an abstract class e.g. BaseTest in which this Rule and all other general test related things can be added. All your test classes should extend this class and by that the rule is applied to all your test cases.