Skip to content

Instantly share code, notes, and snippets.

@beatngu13
Last active May 14, 2024 13:43
Show Gist options
  • Save beatngu13/8d5f60355d7fbef143198f020b1efca3 to your computer and use it in GitHub Desktop.
Save beatngu13/8d5f60355d7fbef143198f020b1efca3 to your computer and use it in GitHub Desktop.
Dead-simple cross-browser testing with Selenium and JUnit 5 parameterized tests
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.openqa.selenium.WebDriver;
class CrossBrowserTest {
@ParameterizedTest
@MethodSource( "my.package.WebDriverFactory#getAll" )
void cross_browser_test( final WebDriver driver ) {
System.out.println( "Test with " + driver.getClass().getSimpleName() );
}
}
import java.util.Arrays;
import java.util.stream.Stream;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
public class WebDriverFactory {
enum Driver {
CHROME,
FIREFOX
// add more drivers
}
public static WebDriver get( final Driver driver ) {
// add default options
switch ( driver ) {
case CHROME:
return new ChromeDriver();
case FIREFOX:
return new FirefoxDriver();
default:
throw new IllegalArgumentException( "No '" + driver + "' driver available." );
}
}
public static Stream<WebDriver> getAll() {
return Arrays.stream( Driver.values() ).map( WebDriverFactory::get );
}
}
@martin-v
Copy link

Or even simpler

public class WebDriverFactory {
  public static Stream<WebDriver> drivers() {
    return Stream.of( new ChromeDriver(), new FirefoxDriver() );
  }
}

@beatngu13
Copy link
Author

Yep, that's sufficient if you don't need a factory method for a single driver (WebDriverFactory#driver( Driver )).

@jackeymason
Copy link

Thank you for putting this together.
I never would have considered the use of a Stream for this. This lead to me to exploring lambda expressions.
Regarding: "WebDriverFactory::driver"
This doesn't compile for me. Shouldn't the double colon be followed by method? Maybe you meant "get" ?
Tested with WebDriverFactory:get and it worked great.
Many thanks.

@beatngu13
Copy link
Author

Hey @jackeymason, glad you like the approach. And you are right, ::driver was a typo, which was the old method name. Thanks for pointing this out!

@Vivek-Malhotra
Copy link

do we have complete code. for .e.g I just need to run a simple login test using webdriver on two browsers. where do I pass the browser option to @test Method?

@beatngu13
Copy link
Author

@Vivek-Malhotra your @Test becomes a @ParameterizedTest (see https://gist.github.com/beatngu13/8d5f60355d7fbef143198f020b1efca3#file-crossbrowsertest-java-L7), then you can specify the WebDriver parameter (see https://gist.github.com/beatngu13/8d5f60355d7fbef143198f020b1efca3#file-crossbrowsertest-java-L9).

Or what exactly are you struggling with?

@Vivek-Malhotra
Copy link

Thanks, @beatngu13
I will try, but is there a way to do it with Junit-4 also. I have an existing framework with a large set of test suite which on Junit-4. I am not sure right now, if switching to Junit-5 will break anything, but will try this option also.

@standbyoneself
Copy link

Could it work with BeforeEach/BeforeAll annotations? I have a class with fixtures about running Playwright, creating browser, closing browser after all tests etc.

@beatngu13
Copy link
Author

@standbyoneself I'm not familiar with the Playwright API, do you have an example / some code to look at?

@standbyoneself
Copy link

Yes. Here is a class with tests:

import java.util.Date;

import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import io.qameta.allure.Issue;
import io.qameta.allure.TmsLink;
import ru.sbrf.rmc.annotations.Certificate;
import ru.sbrf.rmc.models.AdminPagePath;
import ru.sbrf.rmc.pages.AdminPage;

@DisplayName("Админка")
public class AdminPageTests extends PageTestFixtures {
    private AdminPage adminPage;

    @BeforeEach
    public void createPage() {
        adminPage = new AdminPage(page);
    }

    @Nested
    @DisplayName("Объекты")
    class Subjects {
        private void shouldCreateAndDeleteSubject(String prefix, String searchQuery) {
            long timestamp = new Date().getTime();
            String name = prefix + "-" + timestamp;
            adminPage.goTo(AdminPagePath.SUBJECTS);
            adminPage.goToSubjectForm();
            adminPage.fillSubjectName(name);
            adminPage.fillSubjectSearchQuery(searchQuery);
            adminPage.submitSubjectForm();
            adminPage.waitFor(AdminPagePath.SUBJECTS);
            adminPage.checkIfSubjectIsVisible(name);
            adminPage.deleteSubject(name);
        }

        @ParameterizedTest(name = "{displayName}")
        @Certificate("curuser-6")
        @Issue("RMC-6335")
        @TmsLink("RMC-T6354")
        @CsvSource({"Subject,playwright OR selenium"})
        @DisplayName("Создание и удаление объекта с ролью \"Бизнес-администратор\"")
        public void shouldCreateAndDeleteSubjectByFeedAdmin(String prefix, String searchQuery) {
            shouldCreateAndDeleteSubject(prefix, searchQuery);
        }
    
        @ParameterizedTest(name = "{displayName}")
        @Certificate("curuser-10")
        @Issue("RMC-6335")
        @TmsLink("RMC-T6355")
        @CsvSource({"Subject,playwright OR selenium"})
        @DisplayName("Создание и удаление объекта с ролью \"Куратор процесса\"")
        public void shouldCreateAndDeleteSubjectByProcessManager(String prefix, String searchQuery) {
            shouldCreateAndDeleteSubject(prefix, searchQuery);
        }
    }
}

And a fixture class:

import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.*;

import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Cookie;

import ru.sbrf.rmc.AuthClient;
import ru.sbrf.rmc.AuthClientImpl;
import ru.sbrf.rmc.annotations.Certificate;
import ru.sbrf.rmc.mappers.CookieMapperImpl;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PageTestFixtures {
    private Playwright playwright;
    private Browser browser;
    private BrowserContext context;
    protected Page page;
    private AuthClient authClient = new AuthClientImpl(new CookieMapperImpl());

    private String getCertificateName(TestInfo testInfo) throws NoSuchMethodException {
        Optional<Method> optionalMethod = testInfo.getTestMethod();

        if (optionalMethod.isEmpty()) {
            throw new NoSuchMethodException();
        }

        Method method = optionalMethod.get();
        Certificate annotation = method.getAnnotation(Certificate.class);
        return annotation.value();
    }

    @BeforeAll
    public void launchBrowser() {
        playwright = Playwright.create();
        browser = playwright.chromium().launch();
    }

    @AfterAll
    public void closeBrowser() {
        playwright.close();
    }

    @BeforeEach
    public void createContextAndPage(TestInfo testInfo) {
        context = browser.newContext(new Browser.NewContextOptions()
            .setBaseURL(System.getenv().getOrDefault("BASE_URL", "https://rmp-ift.sberbank.ru/lenta/"))
            .setIgnoreHTTPSErrors(true));
        page = context.newPage();

        try {
            String certificateName = getCertificateName(testInfo);
            List<Cookie> cookies = authClient.getSessionCookies(certificateName);
            context.addCookies(cookies);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @AfterEach
    public void closeContext() {
        context.close();
    }
}

@standbyoneself
Copy link

standbyoneself commented May 3, 2024

I have used @ParameterizedTest in couple with @​ValueSource(strings = {"chromium","firefox"}), but I have to explicitly call setupBrowser() in each test, which is really annoying. Like that:

Test

        @ParameterizedTest(name = "{displayName}")
        @Certificate("curuser-6")
        @Issue("RMC-6335")
        @TmsLink("RMC-T6354")
        @ValueSource(strings = {"chromium","firefox"})
        @DisplayName("Создание и удаление объекта с ролью \"Бизнес-администратор\"")
        public void shouldCreateAndDeleteSubjectByFeedAdmin(String browser) {
            setupBrowser(browser);
            shouldCreateAndDeleteSubject();
        }

Fixture

    protected void setupBrowser(String browserType) {
        playwright = Playwright.create();
        if (browserType.equals("chromium")) {
            browser = playwright.chromium().launch();
        } else if (browserType.equals("firefox")) {
            browser = playwright.firefox().launch();
        } else {
            throw new IllegalArgumentException();
        }
        createContextAndPage();
    }

Really want to have ability to use BeforeEach/AfterAll annotations for better readability and non-verbose reusability

@beatngu13
Copy link
Author

@standbyoneself I just had a look at the Playwright docs. Are you aware that there is (experimental) Junit 5 support? Apparently, it does all the lifecycle management for you. And for cross-browser testing, maybe OptionsFactory can help?

@standbyoneself
Copy link

@beatngu13 I am using a custom fixture, so I am afraid of experimental fixtures, also i need to customize it

@standbyoneself
Copy link

Ended up with running scripts in parallel for local development

test.sh

#!/bin/sh

BROWSER=chromium mvn test & BROWSER=firefox mvn test & BROWSER=webkit mvn test & wait

PageTestFixtures.java

    private void updateAllureTestNameAndHistoryId(String baseName) {
        Allure.getLifecycle().updateTestCase(testResult -> {
            String name = String.format("%s: %s", baseName, testResult.getName());
            String historyId = UUID.nameUUIDFromBytes(name.getBytes()).toString();
            testResult.setName(name);
            testResult.setHistoryId(historyId);
        });
    }
    
    @BeforeAll
    public void launchBrowser() {
        playwright = Playwright.create();
        BrowserFactory browserFactory = new BrowserFactoryImpl(playwright);
        String browserName = System.getenv().getOrDefault("BROWSER", "chromium");
        browserType = browserFactory.createBrowserType(browserName);
        BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions();
        Optional<String> browserPath = Optional.ofNullable(System.getenv("BROWSER_PATH"));
        if (browserPath.isPresent()) {
            launchOptions.setExecutablePath(Paths.get(browserPath.get()));
        }
        browser = browserType.launch(launchOptions);
    }
    
    @BeforeEach
    public void createContextAndPage(TestInfo testInfo) {
        updateAllureTestNameAndHistoryId(String.format("%s %s", browserType.name(), browser.version()));
        context = browser.newContext(new Browser.NewContextOptions()
            .setBaseURL(System.getenv().getOrDefault("BASE_URL", "https://rmp-ift.sberbank.ru/lenta/"))
            .setIgnoreHTTPSErrors(true));
        page = context.newPage();

        try {
            String certificateName = getCertificateName(testInfo);
            List<Cookie> cookies = authClient.getSessionCookies(certificateName);
            context.addCookies(cookies);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Keeping consistent historyId allows to view retries in Allure Report

In CI/CD e.g. Jenkins we are gonna using Jenkins Matrix

Hope it'll be helpful for somebody

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment