t.me/atinfo_chat Telegram группа по автоматизации тестирования

Параллельный запуск UI тестов (testng + maven)

testng
java
maven
Теги: #<Tag:0x00007f21d3d82a88> #<Tag:0x00007f21d3d82948> #<Tag:0x00007f21d3d82808>

(Evgeny Pichugin) #1

Доброго дня. В связи с увеличением покрытия тестов появилась мысль попробовать их распараллелить. Нахожусь пока на стадии сбора информации. :slight_smile:

Немного о запуске:
Запускаем тесты в основном только под chromedriver последних версий. Редко используем иные браузеры, но в далеком будущем хотелось бы иметь выбор.
Используем TC, агенты которого динамичны. При запросе поднимаются с нужным buld env.
Соотвественно, все тесты будут запускаться на одного агенте в несколько потоков. Количество потоков хотелось бы регулировать.

Главное требование: чтобы тесты внутри тестового класса запускались последовательно.
Немного о проекте. Используем собственную обвязку над selenide.
Пример testng.xml:

> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="WAY4Web 2.0 Functional Tests (full pack)" parallel="false" configfailurepolicy="continue">
    <test name="Merchant Portal" preserve-order="true" enabled="true">
        <classes>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantPortal.MerchantPortal" />
        </classes>
    </test>
    <test name="RiskManagement" preserve-order="true">
        <parameter name="tms.test.code" value="RiskManagement"/>
        <classes>
            <class name="com.openwaygroup.qa.w4w.UI2.RiskManagement.RiskManagement"/>
            <class name="com.openwaygroup.qa.w4w.UI2.RiskManagement.RiskCase"/>
        </classes>
    </test>
    <test name="Merchant Service Workbench" preserve-order="true">
        <parameter name="tms.test.code" value="MerchantServiceWorkbench"/>
        <classes>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Workflow.Actions"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Actions.ChangeMerchant"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Actions.CreateContract"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Actions.ChangeParentContract"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Actions.DefineDeviceParameters"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.MerchantServiceConfiguration.GlobalParameters"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.FinancialTabs.Transactions"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.FinancialTabs.TransactionQueries"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.ActivityNotices.SearchNotification"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.ActivityNotices.MerchantMessages"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.ActivityNotices.ClerkNotices"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.ActivityNotices.NoticesByFlow"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.MSWMerchants"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.MSWSearch"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.MerchantServiceWorkbench"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.FinancialTabs.DocumentActions"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Tabs.Cycles"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Tabs.AdditionalInfo"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Tabs.Addresses"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Tabs.Billing"/>
            <class name="com.openwaygroup.qa.w4w.UI2.MerchantServiceWorkbench.Tabs.FileExchange"/>
        </classes>
    </test>

Пример одного из тестовых классов, кейсы внутри которого должны запускаться последовательно:

public class Transactions extends W4WUITestTF {

	private static String merchantName = "AutotestMerchant";

	public Transactions() {
		user = "W4WEBUI";
		//user = "dep_admin";
		password = "abwx";
	}

	@Test(testName = "0010 - if transaction is not order-based, show it", priority = 1, alwaysRun = true)
	public void notOrderBasedTransaction() {
		navigateToMerchantDetails();
		navigateByContractTree("POS");

		checkTransaction("Retail", "777.00 (RUR)");
	}

	@Test(testName = "0020 - if transaction is order-based and is Misc, show it", priority = 2, alwaysRun = true, enabled = false)
	public void orderBasedAndMiscTransaction() {
		navigateToMerchantDetails();
		navigateByContractTree("SubAccount");

		//TODO: wait for discussion is finished in WWEB-6189
	}

Вот так выглядит класс, который экстендит тест:

package com.openwaygroup.qa.w4w.tfw;

import com.openwaygroup.qa.w4w.testing.w4wProduct;
import org.openqa.selenium.logging.LogType;
import org.testng.IHookCallBack;
import org.testng.IHookable;
import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;

import java.util.logging.Level;

import static com.codeborne.selenide.Selenide.getWebDriverLogs;
import static org.testng.ITestResult.FAILURE;

/**
 * Base class for WAY4Web UI functional tests.
 *
 * @author rzakirov at 12.04.2017.
 */
public class W4WUITestTF extends W4WTestTF implements IHookable {

	@BeforeClass
	public void before(){
		setProduct(new w4wProduct());
	}

	/**
	 * Logins to the WAY4Web portal.
	 * Runs before any test method in the test class.
	 */
	@BeforeMethod
	public void login() {
		engine.login(config.getUsername(), config.getPassword());
	}

	/**
	 * Logouts from the WAY4Web portal.
	 * Runs after any test method in the test class.
	 */
	@AfterMethod
	public void logout() {
		engine.logout();
	}

	/**
	 * Checks the test run status.
	 * If test failed, then makes a browser window screenshot.
	 * Used instead of @AfterMethod annotated method, which processed after Allure test listener in TestNG adaptor.
	 *
	 * @param callBack   Hook Callback
	 * @param testResult Test run result
	 */
	@Override
	public void run(IHookCallBack callBack, ITestResult testResult) {
		callBack.runTestMethod(testResult);
		getWebDriverLogs(LogType.BROWSER, Level.SEVERE).forEach(LOG::info);
		if (testResult.getThrowable() != null) {
			engine.captureCurrent("Test failed");
		}
		if (engine.hasAssertionErrors()) {
			AssertionError assertionException = engine.getSoftAssertionError();
			LOG.error(assertionException.getMessage());
			testResult.setStatus(FAILURE);
			if (testResult.getThrowable() == null) {
				testResult.setThrowable(assertionException);
			}
		}
	}

	/**
	 * Executes the provided code catching the exceptions and making screenshot on error.
	 *
	 * @param errorTitle Screenshot title
	 * @param command    Runnable code
	 */
	protected void execute(String errorTitle, Runnable command) {
		try {
			command.run();
		} catch (RuntimeException | AssertionError ex) {
			engine.captureCurrent(errorTitle);
			throw ex;
		}
	}
}

Так выглядит класс, который экстендит класс, который экстендит тест:

package com.openwaygroup.qa.w4w.tfw;

import com.codeborne.selenide.Configuration;
import com.google.common.collect.ImmutableMap;
import com.openwaygroup.jsengine.tf.w4w.Engine;
import com.openwaygroup.jsengine.tf.w4w.EngineFactory;
import com.openwaygroup.qa.w4w.testing.w4wProduct;
import com.openwaygroup.tf.TestContext;
import com.openwaygroup.tf.ant.PropMan;
import com.openwaygroup.tf.file.TextFile;
import com.openwaygroup.tf.testing.ATest;
import com.openwaygroup.tf.util.Resource;
import org.apache.commons.text.StringSubstitutor;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.ITestContext;
import org.testng.ITestResult;
import org.testng.annotations.*;

import java.io.File;
import java.lang.reflect.Method;

import static com.openwaygroup.qa.w4w.testing.w4wProduct.W4W_VERSION;

/**
 * Base class for WAY4Web functional tests.
 */
public abstract class W4WTestTF extends ATest{

    protected String user;
    protected String password;

    /**
     * SLF4J Logger.
     */
	public static final Logger LOG = LoggerFactory.getLogger(W4WTestTF.class);

    /**
     * Test configuration settings.
     */
    protected TestConfig config;

    /**
     * Test Engine.
     */
    protected Engine engine;


    public static String getW4WMajorVersion()
    {
        String w4wversion = PropMan.getProperty(W4W_VERSION);
        String majorVersion = w4wversion.split("\\.")[0];
        return majorVersion;
    }


    @BeforeSuite
    public void setLoggerLevels() {
        Configurator.setLevel("com.openwaygroup.tf.ant", Level.INFO);
        Configurator.setLevel("com.openwaygroup.tf.file", Level.TRACE);
        Configurator.setLevel("org.apache.commons", Level.OFF);
        Configurator.setLevel("org.apache.http", Level.OFF);
        Configurator.setLevel("io.netty", Level.OFF);
        Configurator.setLevel("org.littleshoot", Level.ERROR);
        Configurator.setLevel("net.lightbody", Level.OFF);
        Configurator.setLevel("com.codeborne.selenide", Level.OFF);
        Configurator.setLevel("org.openqa.selenium", Level.OFF);

        Configurator.setLevel("com.openwaygroup.jsengine.tf.w4w.Navigation2", Level.DEBUG);
        Configurator.setLevel("com.openwaygroup.jsengine.tf.w4w.Navigation3", Level.DEBUG);
    }

    /**
     * Creates and configuration Test Engine.
     * Sets the current test suite name into the engine.
     * Runs before any test method in the test class.
     */
    @BeforeClass
    public void setUp() {
        LOG.info("Test class: {}", getClass().getSimpleName());
        config = new TestConfig(getDataFilePath());
        engine = EngineFactory.getEngine(getEngineFilePath());
        engine.suite(getClass().getSimpleName());
        engine.step("setUp");
		if(isW4W3()) {
			Configuration.pageLoadStrategy = "none";
		}
    }

    /**
     * Sets the current test name into the engine.
     * Runs prior to test method.
     *
     * @param m Test method
     * @param ctx Test context
     */
    @BeforeMethod
    public void start(Method m, ITestContext ctx) {
        LOG.info("Test: {}", getTestName(m));
        String testName = new String(getTestName(m));
        com.codeborne.selenide.Configuration.reportsFolder = (TestContext.getLogsFolder() + testName).replace("\\", "\\\\");
        engine.step(m.getName());
    }

    /**
     * Checks the test run status.
     * If test failed, then log error message.
     * Runs after test method.
     *
     * @param m Test method
     * @param result Test run result
     */
    @AfterMethod
    public void end(Method m, ITestResult result) {
        if (result.getStatus() == ITestResult.FAILURE) {
            LOG.error("Test failed", result.getThrowable());
        }
    }

    /**
     * Closes database connection, which could be opened during the tests.
     * Runs after all test methods in the test class.
     */
    @AfterClass
    public void cleanup() {
        engine.dbClose();
    }

    /**
     * Creates immutable map builder.
     * Can be used to create immutable {@link java.util.Map} from provided parameters.
     *
     * @return Immutable map builder
     */
    protected static ImmutableMap.Builder<String, Object> map() {
        return ImmutableMap.builder();
    }

    /**
     * Extracts test name from the method's {@link Test} annotation's 'testName' attribute.
     * If the attribute not found, then uses test method name.
     *
     * @param m Test method
     * @return Test name
     */
    protected String getTestName(Method m) {
        String testName;
        Test testAnnotation = m.getAnnotation(Test.class);
        if (testAnnotation != null) {
            testName = testAnnotation.testName();
            name = testName;
        } else {
            testName = m.getName();
        }
        return testName;
    }

    /**
     * Reads System property 'w4wtest.data' to calculate test data file path.
     *
     * @return File path to test data.
     */
    protected String getDataFilePath() {
        PropMan.putProperty("ui.user.username", user);
        PropMan.putProperty("ui.user.password", password);
        PropMan.putProperty("db.jdbc.url", PropMan.getActiveProperty("db.url"));
        PropMan.putProperty("db.jdbc.username", PropMan.getActiveProperty("db.owner"));
        PropMan.putProperty("db.jdbc.password", PropMan.getActiveProperty("db.ownpwd"));

        return processResource("ui.test.properties");
    }

    /**
     * Reads System property 'w4wtest.engine' to calculate engine configuration file path.
     *
     * @return File path to engine configuration.
     */
    protected String getEngineFilePath() {
        PropMan.putProperty("browser", PropMan.getProperty("browser"));
        String sitePort = PropMan.getActiveProperty("siteport");
        if (sitePort==null){
            PropMan.putProperty("app.host.url", "http://" + PropMan.getActiveProperty("remote_deploy_host") + ":" + w4wProduct.getW4WSeriesID() + "00");
        }
        else {
            PropMan.putProperty("app.host.url", "http://" + PropMan.getActiveProperty("remote_deploy_host") + ":" + sitePort);
        }
        String logs_folder = TestContext.getLogsFolder().replace("\\", "\\\\");
        PropMan.putProperty("tfw.logs.path", logs_folder);
        return processResource("ui" + getW4WMajorVersion() + ".engine.properties");
    }

    private String processResource(String fileName){
        String fileContext = Resource.get(W4WTestTF.class,fileName);
        StringSubstitutor sub = new StringSubstitutor(PropMan.getPropertiesAsMap());
        TextFile propFile = new TextFile(TestContext.getCurrentFolder() + File.separator + fileName);
        propFile.setContent(sub.replace(fileContext));
        return propFile.getPath();
    }

    /**
     * Checks if test version of WAY4Web is 2.0.
     *
     * @return true if WAY4Web version is 2.0.
     */
    protected boolean isW4W2() {
        return "2".equals(PropMan.getActiveProperty("w4wversion").split("\\.")[0]);
    }

    /**
     * Checks if test version of WAY4Web is 3.0.
     *
     * @return true if WAY4Web version is 3.0.
     */
    protected boolean isW4W3() {
        return "3".equals(PropMan.getActiveProperty("w4wversion").split("\\.")[0]);
    }
}

Жду ваших советов, вопросов и предложений. Спасибо. :slight_smile:


(Nik Sidorenko) #2

Для параллельности сразу посмотрите в сторону Selenoid + GGR (Go Grid Router)
Он и с покрытием браузеров поможет. И видео тестов запишет.

Последовательный запуск тестов одного класса обеспечивается настройками TestNG (xmlSuite.setParallel(XmlSuite.ParallelMode.CLASSES))
Есть нюансы с запуском тестов с дата провайдерами. Если столкнётесь, спрашивайте.

Количество потоков регулируется тем же TestNG (xmlSuite.setThreadCount(<желаемое количество потоков>) + xmlSuite.setDataProviderThreadCount(<желаемое количество потоков для дата провайдеров>))

Также позаботьтесь о переводе драйвера на ThreadLocal https://ru.stackoverflow.com/questions/633320/Зачем-нужен-класс-threadlocal-в-java


(Sheff) #3
    @Test
    public void runAllTests() {
        Class<?>[] classes = { ParallelTest1.class, ParallelTest2.class };

        // ParallelComputer(true,true) will run all classes and methods 
        // in parallel.  (First arg for classes, second arg for methods)
        JUnitCore.runClasses(new ParallelComputer(true, true), classes);
    }

#4

Як варіант можна аикористати maven-surefire-plugin, в нього передати TestNG .xml файл з тестами які потрібно виконати. Паралельність і кількість потоків вказується в цьому .xml-і:

<suite name="Suite" parallel="methods" thread-count="5">
    <test name="Test">
        <classes>
            <class name="tests.FirstParallelTest" />
        </classes>
    </test>
</suite>