Advertisement
Advertisement

More Related Content

Advertisement

Of plugins and decorators - trivago's e2e test framework in the spotlight

  1. @bischoffdev Of Plugins and Decorators Selenium Conf Online 2022 Benjamin Bischoff, trivago N.V.
  2. @bischoffdev Agenda • Introduction • Why a new framework? • The challenges and solutions • Last thoughts
  3. @bischoffdev Disclaimer • This talk focusses on technology • The code examples are in Java • What works for us might not work for you • Good solutions solve speci fi c problems
  4. @bischoffdev Introduction
  5. @bischoffdev About me • Benjamin Bischoff • Test automation engineer @ trivago • 22 years in tech (7 as SDET) • softwaretester.blog
  6. @bischoffdev About trivago • Founded in 2005 • Located in Düsseldorf, Germany • First German hotel meta search
  7. @bischoffdev
  8. @bischoffdev Why a new Framework?
  9. @bischoffdev Why a New Framework? • Too bloated • Too chaotic • Not able to handle A/B testing • Hard to solve company speci fi c problems
  10. @bischoffdev Building a framework • What problems should it solve? • Who is the target audience? • Which dependencies exist? • What does it have to interface with? • Where should it run? • How should it be triggered • etc…
  11. @bischoffdev Why Selenium? • Proven browser automation technology • Compatible with most browsers/devices • Can handle multiple windows and tabs • Standardised protocol
  12. @bischoffdev The test framework • Pure Java framework - started in 2016 • Based on latest Cucumber BDD and Selenium versions • Supports various browser/OS combinations and mobile devices • Plugin API for custom functionality
  13. @bischoffdev The Challenges
  14. @bischoffdev Element resilience & hierarchy
  15. @bischoffdev Selenium WebElement • The building block of all Selenium code • Findable via locators (By) • Supports various interactions
  16. @bischoffdev WebElement Challenges • No automatic stale element handling 
 (and I get why) • No element hierarchy 
 (now it is kind of there) • One fi xed standard timeout 
 (I am not talking about fl uent waits)
  17. @bischoffdev Solution: Custom WebElement
  18. @bischoffdev Page Objects vs Page Components
  19. @bischoffdev Page Objects • Wrapping page functionality into classes • Interactions through a single entry point • Encapsulated units of code searchPage.searchFor(“London”);
  20. @bischoffdev Page Components • Wrapping component functionality into classes • Combining components into pages • Delegating actions to components through the page searchPage.search.searchFor(“London”);
  21. @bischoffdev Page and component relationship package com.trivago.pages; import com.trivago.base.Page; import com.trivago.components.Search; import com.trivago.components.Footer; @Component @ScenarioScope public class SearchPage extends Page { public Search search() { return getComponent(Search.class); } public Footer footer() { return getComponent(Footer.class); } } Page @Component public class Search extends Component { public static final String SEARCH_CSS = "[data-testid='search-form']"; public static final String BUTTON_CSS = "[data-testid='search-button']"; @FindBy(css = SEARCH_CSS) private CustomWebElement parent; public void clickSearchButton() { parent .findElementByCssSelector (BUTTON_CSS).click(); } } Page component
  22. @bischoffdev Component and web element fl ow Request component from the Component Factory Apply custom FieldDecorator on @FindBy elements Retrieve component from the Spring context First interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Register callback on the component for method interception Initialize custom WebElement properties (e.g. By condition) Subsequent interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Skip initialization Execute method using initialized properties Execute method using initialized properties
  23. @bischoffdev Component Factory <T extends Component> T getComponent(final Class<T> requestedComponentClass) { T pageComponent = (T) applicationContext.getBean(requestedComponentClass); FieldDecorator fieldDecorator = new CustomFieldDecorator(webDriverProvider.getCurrentWebDriver(), properties); org.openqa.selenium.support.PageFactory.initElements(fieldDecorator, pageComponent); return pageComponent; } Requesting a component public Search search() { return getComponent(Search.class); } Component access through page
  24. @bischoffdev –Simon Steward, 2017-06-30, Twitter „The PageFactory design ain't my best work. I'd recommend using it for inspiration for something better.“
  25. @bischoffdev Component and web element fl ow Request component from the Component Factory Apply custom FieldDecorator on @FindBy elements Retrieve component from the Spring context First interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Register callback on the component for method interception Initialize custom WebElement properties (e.g. By condition) Subsequent interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Skip initialization Execute method using initialized properties Execute method using initialized properties
  26. @bischoffdev public class CustomFieldDecorator implements FieldDecorator { private final FieldDecorator defaultFieldDecorator; private final WebDriver webDriver; private final PropertiesMap propertiesMap; public CustomFieldDecorator(final WebDriver driver, final PropertiesMap propertiesMap) {} @Override public Object decorate(final ClassLoader loader, final Field field) { if (CustomWebElement.class.isAssignableFrom(field.getType()) && field.isAnnotationPresent(FindBy.class)) { Class<?> parentClass = field.getDeclaringClass(); Enhancer e = new Enhancer(); e.setSuperclass(field.getType()); e.setCallback(new CustomElementHandler(field, new DefaultElementLocatorFactory(webDriver) .createLocator(field), webDriver, propertiesMap, parentClass) ); return e.create(); } else { return defaultFieldDecorator.decorate(loader, field); } } FieldDecorator
  27. @bischoffdev Component and web element fl ow Request component from the Component Factory Apply custom FieldDecorator on @FindBy elements Retrieve component from the Spring context First interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Register callback on the component for method interception Initialize custom WebElement properties (e.g. By condition) Subsequent interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Skip initialization Execute method using initialized properties Execute method using initialized properties
  28. @bischoffdev public Object intercept(final Object instance, final Method method, final Object[] objects, final MethodProxy methodProxy) throws Throwable { if (instance instanceof CustomWebElement) { CustomWebElement customWebElement = (CustomWebElement) instance; if (!customWebElement.isInitialized()) { WebElement element = locator.findElement(); customWebElement.setWebElement(element); customWebElement.setWebDriver(webDriver); customWebElement.setProperties(propertiesMap); customWebElement.setBy(getFindByCondition(field.getName())); } try { return methodProxy.invokeSuper(instance, objects); } catch (InvocationTargetException e) { throw e.getCause(); } } if (instance instanceof WebElement) { WebElement displayedElement = locator.findElement(); return method.invoke(displayedElement, objects); } return null; Custom element handler
  29. @bischoffdev Component and web element fl ow Request component from the Component Factory Apply custom FieldDecorator on @FindBy elements Retrieve component from the Spring context First interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Register callback on the component for method interception Initialize custom WebElement properties (e.g. By condition) Subsequent interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Skip initialization Execute method using initialized properties Execute method using initialized properties
  30. @bischoffdev public void click() { performConsumerAction( function -> { waitUntilElementIsClickable(); webElement.click(); } ); } 
 private void performConsumerAction( final Consumer<Void> action) { try { highlightElement(webElement); action.accept(null); } catch (StaleElementReferenceException e) { refindWebElement(); highlightElement(webElement); action.accept(null); } } • Element actions use the Selenium WebElement • All actions are passed as consumers • This allows for more concise code Performing element actions
  31. @bischoffdev public void refindWebElement( final CustomWebElement elementToFind) { CustomWebElement parent = elementToFind.parentElement; if (parent != null) { try { parent.webElement.isEnabled(); webElement = parent.webElement.findElement( elementToFind.findByCondition); return; } catch ( StaleElementReferenceException | InvalidArgumentException e ) { refindWebElement(parent); } } webElement = webDriver.findElement( elementToFind.findByCondition); } • If there is a parent, fi nd the element within the parent • If the parent is stale, fi nd the parent again fi rst • If the element does not have a parent, fi nd the element using the web driver Re fi nding elements
  32. @bischoffdev public CustomWebElement findElementDirectly(final By by) { setWebdriverMinimalWait(); CustomWebElement element; try { element = findElement(by); } finally { setWebdriverRegularWait(); } return element; } • Sets the web driver wait to 50ms • Finds the element • Resets the webdriver wait • Yes, I know. Finding elements directly
  33. @bischoffdev Component and web element fl ow Request component from the Component Factory Apply custom FieldDecorator on @FindBy elements Retrieve component from the Spring context First interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Register callback on the component for method interception Initialize custom WebElement properties (e.g. By condition) Subsequent interaction with the custom WebElement FieldDecorator intercepts method call on custom WebElement Skip initialization Execute method using initialized properties Execute method using initialized properties
  34. @bischoffdev public Object intercept(final Object instance, final Method method, final Object[] objects, final MethodProxy methodProxy) throws Throwable { if (instance instanceof CustomWebElement) { CustomWebElement customWebElement = (CustomWebElement) instance; if (!customWebElement.isInitialized()) { WebElement element = locator.findElement(); customWebElement.setWebElement(element); customWebElement.setWebDriver(webDriver); customWebElement.setProperties(propertiesMap); customWebElement.setBy(getFindByCondition(field.getName())); } try { return methodProxy.invokeSuper(instance, objects); } catch (InvocationTargetException e) { throw e.getCause(); } } if (instance instanceof WebElement) { WebElement displayedElement = locator.findElement(); return method.invoke(displayedElement, objects); } return null; Custom element handler
  35. @bischoffdev Why so complex? • We wanted to be free to use either 
 our own custom WebElement or Selenium’s WebElement • We did not want to introduce another annotation to keep usage straight 
 forward • Remember: it is only complex for the framework creators!
  36. @bischoffdev Challenge: Extensibility
  37. @bischoffdev Run Test Scenario Perform Actions Open Local Browser Make Assertions Log Results Queue Tests for Re-Run Send Failure Message Open Browser in Test Cloud Challenge: Extensibility
  38. @bischoffdev Direct Framework Integration? • Too much functionality in core framework • Tightly bound to speci fi c cloud vendors • Functionality changes need framework architecture knowledge • Every additional feature causes new framework releases
  39. @bischoffdev Solution: Plugin API
  40. @bischoffdev Requirements • Writing plugins should be simple • Should be small separate projects • New plugins without core changes • Should not in fl uence core functionality
  41. @bischoffdev Plugin characteristics • Plugin implement interfaces of interest • Framework noti fi es matching plugins • Each plugin has its own set of properties • Plugins can be activated as needed
  42. @bischoffdev Service Provider Interface (SPI) • Available for 22 years (since Java 1.3) • Three parts: • Service Interface 
 Providers need to implement this • Provider Registration API 
 For registering implementations • Service Access API 
 Allows clients to access service instances
  43. @bischoffdev package com.trivago.plugin.extension; /** * Interface for all plugins. * All plugins need to return their name for logging * and plugin specific property handling. */ public interface Plugin { String getPluginName(); } • Base interface for all plugin interfaces • Returns the plugin name • Allows for easy plugin discovery Basic plugin interface
  44. @bischoffdev Plugin „marker fi le“ • Used by the ServiceProvider to 
 fi nd implementations com.trivago.plugin.CustomCloudPlugin src/java/resources/META-INF/services/ com.trivago.plugin.extension.Plugin
  45. @bischoffdev package com.trivago.plugin; import com.trivago.plugin.extension.Plugin; import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader; class PluginLoader { private final List<Plugin> pluginList; PluginLoader() { pluginList = new ArrayList<>(); for (Plugin plugin : ServiceLoader.load( 
 Plugin.class)) { pluginList.add(Plugin); } } } Plugin Loader • Plugin loader searches all classes implementing the base Plugin interface • It uses the de fi nitions in META-INF • The plugin loader is the central interface to all plugins
  46. @bischoffdev package com.trivago.plugin.extension; import org.openqa.selenium.WebDriver; /** * Executed after the WebDriver is created. * Used in plugins which need to use the WebDriver * directly (e.g. for screenshots). */ public interface WebDriverCreatedAware extends Plugin { void handleWebDriverCreated(final WebDriver driver); } • Interface for noti fi cations of a created webdriver • Passes the driver to all interested plugins • This can be used for screenshots, retrieving webdriver logs, etc. Plugin interface for a created WebDriver
  47. @bischoffdev public class DemoPlugin implements WebDriverCreatedAware, CucumberAfterStepHookAware { private WebDriver webdriver; @Override public void handleWebDriverCreated( final WebDriver driver) { this.webdriver = driver; } @Override public void handleCucumberAfterHook( final Scenario scenario, final List<Steps> steps ) { storeScreenshot( ((TakesScreenshot) webdriver) .getScreenshotAs(OutputType.BYTES)) } } Sample plugin Plugin noti fi cation public WebDriver createWebDriver() { driver = webDriverCreator 
 .createWebDriver(); pluginManager .notifyWebDriverCreatedPlugins(driver); return driver; } Plugin noti fi cation by framework
  48. @bischoffdev Plugin interfaces • CorePropertiesAware • CucumberBeforeHookAware • CucumberBeforeStepHookAware • CucumberAfterHookAware • CucumberAfterStepHookAware • CloudCapabilitiesAware • CoreCapabilitiesAware • PluginPropertiesAware • WebDriverCreatedAware • WebDriverDestroyedAware • WebdriverPreDestroyedAware • WebdriverPropertiesAware
  49. @bischoffdev Run Test Scenario Perform Actions Open Local Browser Make Assertions Log Results Queue Tests for Re-Run Send Failure Message Open Browser in Test Cloud Challenge: Extensibility
  50. @bischoffdev Plugin Discovery Open Browser Run Test Scenario Perform Actions Make Assertions Plugins Plugin Manager Plugins Plugins Plugins Plugins Register Communicate Notify
  51. @bischoffdev Last thoughts
  52. @bischoffdev Why no commit to Selenium? • Re fi nding elements is out of Selenium’s main scope • It is tied to our speci fi c use cases • Selenium is a remote control 
 for browsers
  53. @bischoffdev –https://www.selenium.dev/ „Selenium automates browsers. That's it!“
  54. @bischoffdev First the problem, then the tool! • Understand the problem fully fi rst • Evaluate tools in regard to your problem
  55. @bischoffdev Know your tools • Before switching to other tools, check if the current one can solve your problem • It can be valuable to dig deeper!
  56. @bischoffdev Don’t dismiss „old“ solutions • Decouple progress from technology • „Old tools“ often means „proven tools“ • Often times no „tractor factor“
  57. @bischoffdev Reinventing the wheel • Not all wheels perform the same • They have to comply with the terrain • You learn a lot about wheels
  58. @bischoffdev Thank you! Selenium Conf Online 2022 www.softwaretester.blog | @bischoffdev
Advertisement