Skip to content

Latest commit

 

History

History
341 lines (239 loc) · 20.4 KB

SeleniumFoundationTestSupport.md

File metadata and controls

341 lines (239 loc) · 20.4 KB

TestNG Listeners

Many of the features provided by Selenium Foundation are driven by TestNG listeners.

  • ListenerChain : This listener, which is declared in TestNG Foundation, enables the addition of other listeners at runtime and guarantees the order in which they're invoked. This is similar in behavior to a JUnit rule chain.
  • ExecutionFlowController : This listener, also found in TestNG Foundation, propagates values stored as test attributes from one phase of test execution to the next.
  • DriverManager : This listener, which is declared in Selenium Foundation itself, performs basic functions related to driver session management.

ListenerChain

Selenium Foundation relies on TestNG Foundation for basic flow control. At the heart of it all is ListenerChain. To provide consistent behavior, we recommend that you activate ListenerChain via the ServiceLoader as described in the TestNG documentation:

org.testng.ITestNGListener
com.nordstrom.automation.testng.ListenerChain

In a Maven project, the preceding file is stored in the src/main/resources folder:

com.testng.ITestNGListener

Once this file is added to your project, ListenerChain will be loaded automatically whenever you run your tests. To attach listeners to the chain, mark your test class with the LinkedListeners annotation:

LinkedListeners annotation
package com.nordstrom.example;
 
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
     
    ...
  
}

As shown above, we use the @LinkedListeners annotation to attach DriverManager and ExecutionFlowController. The order in which listener methods are invoked is determined by the order in which listener objects are added to the chain. Listener before methods are invoked in last-added-first-called order. Listener after methods are invoked in first-added-first-called order. Only one instance of any given listener class will be included in the chain.

ExecutionFlowController

To maintain its settings and state through all phases of each test, Selenium Foundation relies on the ExecutionFlowController listener. This TestNG listener propagates values stored as test attributes from one phase of test execution to the next. A bit of background about TestNG test attribute will be helpful in understanding the purpose of this listener.

Each configuration method (i.e. - @BeforeMethod or @AfterMethod) and each test method executed by TestNG is given its own private data object to play with - the ITestResult object. Among its many responsibilities, the test result object maintains a set of named values - the attributes collection. Selenium Foundation uses this TestNG feature to store test-specific values such as driver instance, initial page object, configuration object, and local Selenium Grid process objects.

The attributes collections are only accessible from the test result object within which they're stored, and each phase of test execution only provides direct access to the "current" test result object - the one owned by the configuration method or test method that's currently being executed. Values stored in the attributes collection of the @BeforeMethod method don't automatically get propagated to the attributes collection of the @Test method. Values stored in the attributes collection of the @Test method don't automatically get propagated to the attributes collection of the @AfterMethod method.

For tests built on Selenium Foundation, we need all of the values stored during each phase of the test to be available to the subsequent phases. The task of propagating test attributes from one phase to the next is handled by ExecutionFlowController. After a @BeforeMethod method or test method is invoked, ExecutionFlowController extracts the attributes collection from this method's result object into its own thread-local storage. Before a test method or @AfterMethod method is invoked, ExecutionFlowController injects the values it stored from the previous phase into the attributes collection of this method's result object.

Note that ExecutionFlowController propagates the entire attributes collection from phase to phase, not just the attributes created by Selenium Foundation. If your test code or page models create test attributes, these will be propagated as well. This provides a convenient, thread-safe way to persist values that are available through the entire test life cycle, which are only visible within the context of the test that created them.

DriverManager

Selenium Foundation includes a TestNG listener (DriverManager) and a static utility class (GridUtility) that perform several basic functions related to Selenium driver session management.

  • Before each @Test method is invoked, DriverManager performs the following tasks:
    • Ensure that a driver instance has been created for the test.
      • NOTE: For local execution, this may spawn a local instance of Selenium Grid.
    • Store the driver instance for subsequent dispensing.
    • Manage configured driver timeout intervals.
    • If specified, open the initial page, storing the page object for subsequent dispensing.
  • @Before... configuration methods can request automatic driver instantiation by specifying an initial page.
    • NOTE: For all @Before.. configuration methods that request automatic driver instantiation (except @BeforeMethod), DriverManager automatically closes the driver after the method is invoked.
    • For @BeforeMethod configuration methods, automatically instantiated drivers are retained and handed off to their respective @Test methods.
  • When each test method finishes (including @AfterMethod, pass or fail), DriverManager performs the following tasks:
    • Terminate any page load operation that's still in progress.
    • Dismiss any browser alert that's currently open.
    • Quit the driver, which will close all open browser windows and end the session.
  • After all tests in the entire suite have finished, DriverManager performs the following tasks:
    • If a Selenium Grid node process was spawned, shut it down.
    • If a Selenium Grid hub process was spawned, shut it down.

Obtaining a Driver

If you've hooked up DriverManager as shown above, a driver will be instantiated for each test method automatically. To retrieve this instance, use one of the provided static methods:

Retrieving the WebDriver instance
package com.nordstrom.example;
 
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
 
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
     
    @Test
    public void testDriverAccess() {
        WebDriver driver = DriverManager.getDriver();
        ...
    }
     
    @AfterMethod
    public void useDriverAfter() {
        WebDriver driver = DriverManager.getDriver();
        ...
    }
    
    private static void doStuff(ITestResult testResult) {
        WebDriver driver = DriverManager.getDriver(testResult);
        ...
    }
}

In the preceding example, DriverManager retrieves the driver attached to the current test. Note that the driver acquired by useDriverAfter() was handed off from testDriverAccess(). For contexts in which the current test is undetermined, the test context can be explicitly specified, as shown in the doStuff() method.

If your test requires a driver that is unavailable via Selenium Grid, or if your scenario requires browser setup that can't be established through the standard Selenium WebDriver API, Selenium Foundation provides two options:

  1. Your test class can implement the DriverProvider interface to replace the default driver instantiation method with one that meets your requirements.
  2. You can decline automatic driver instantiation for an individual test method by specifying the @NoDriver annotation.
Implementing the DriverProvider interface
package com.nordstrom.example;
 
import org.openqa.selenium.WebDriver;
import org.testng.IInvokedMethod;
import org.testng.ITestResult;
 
import com.nordstrom.automation.selenium.interfaces.DriverProvider;
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest implements DriverProvider {
     
    ...
     
    @Override
    public WebDriver provideDriver(IInvokedMethod method, ITestResult testResult) {
        return new VivaldiDriver();
    }
}
Declining automatic driver instantiation with @NoDriver
package com.nordstrom.example;
 
import com.nordstrom.automation.selenium.annotations.NoDriver;
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
     
    @Test
    @NoDriver
    public void noDriverTest() {
        ...
    }
     
    ...
     
}

If your scenario requires a driver in a @Before... configuration method, you can request one from DriverManager via the @InitialPage annotation:

Requesting automatic driver instantiation in @BeforeMethod
package com.nordstrom.example;
 
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
 
import com.nordstrom.automation.selenium.annotations.InitialPage;
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
     
    @BeforeMethod
    @InitialPage(ExamplePage.class)
    public void initialPageFromManager() {
        ExamplePage examplePage = (ExamplePage) DriverManager.getInitialPage();
        NextAppPage nextAppPage = examplePage.performSetup();
        // set initial page for @Test methods
        DriverManager.setInitialPage(nextAppPage);
    }
     
    @Test
    public void initialPageFromBefore() {
        (NextAppPage) nextAppPage = (NextAppPage) DriverManager.getInitialPage();
        ...
    }
     
    ...
  
}

In the preceding example, the initialPageFroManager() method acquires the initial page provided by DriverManager and executes the performSetup() method, resulting in navigation to the next page. This state change is registered by calling setInitialPage(), and the initialPageFromBefore() method acquires this page when it starts. More on initial page in the next section.

Specifying Initial Page

Through the @InitialPage annotation, Selenium Foundation enables you to specify an initial page that should be loaded after instantiating the driver, on either individual test methods or for an entire test class. Note that any page class specified as an initial page must declare its associated URL via the @PageUrl annotation.

Specifying initial page with @InitialPage
package com.nordstrom.example;
 
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
 
import com.nordstrom.automation.selenium.annotations.InitialPage;
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
import com.nordstrom.example.model.HelpPage;
import com.nordstrom.example.model.LoginPage;
 
@InitialPage(LoginPage.class)
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
     
    @Test
    public void testLoginPage() {
        LoginPage loginPage = (LoginPage) DriverManager.getInitialPage();
         
        ...
    }
     
    @Test
    @InitialPage(HelpPage.class)
    public void testHelpPage() {
        HelpPage helpPage = (HelpPage) DriverManager.getInitialPage();
         
        ...
    }
     
    ...
  
}

The preceding example demonstrates how to specify an initial page for all tests in a class, and how to override this specification on a per-test basis. The preceding section includes an example of specifying an initial page for a @BeforeMethod configuration method and recording a different initial page for tests that follow.

Automatic Driver Targeting

For web applications that use frames or multiple windows, a major source of boilerplate code is management of the driver target. In addition to being extremely repetitive, this code is also surprisingly difficult to implement correctly. Selenium Foundation completely eliminates the need for explicit driver targeting. You get to focus on scenario-specific details instead of low-level plumbing.

  • To facilitate automatic driver targeting, Selenium Foundation records the associated browser window handle in every page object.
  • If a page-model method returns a new page object indicating that a new window will be opened for it, Selenium Foundation automatically waits for the new window to open.
  • For frame-based containers, Selenium Foundation retains the strategy used to create them - context element, index, or name/ID.
  • Before invoking a page-model method, Selenium Foundation ensures that the driver is targeting the window or frame associated with the method's container object.
  • To avoid unnecessary target switching, Selenium Foundation maintains a record of the current target object, only switching when the target object changes.

Page Load Synchronization

Selenium Foundation provides both implicit and explicit page load synchronization. For basic plain-vanilla web applications, implicit synchronization is often all you need. For more complex applications with dynamic content (AJAX, single-page applications, etc.), Selenium Foundation defines an interface you can implement to provide scenario-specific detection of page load completion. For more details, see the Page Transition Synchronization section on Building Page Objects with Selenium Foundation.

Driver Lifetime Management and Scenario-Specific Post-Processing

As indicated previously, DriverManager automatically quits the driver when the test method for which it was instantiated is complete. This is done through the following methods of the ITestListener interface:

  • onTestSuccess(ITestResult testResult)
  • onTestSkipped(ITestResult testResult)
  • onTestFailure(ITestResult testResult)
  • onTestFailedButWithinSuccessPercentage(ITestResult testResult)

If you need to perform any post-processing that requires interaction with the browser session (removing test data, logging out, etc.), you have two options - use an @AfterMethod configuration method or a listener that implements ITestListener attached to the listener chain prior to DriverManager.

Scenario-specific post-processing
package com.nordstrom.example;
 
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;
 
import com.nordstrom.automation.selenium.listeners.DriverManager;
import com.nordstrom.automation.testng.ExecutionFlowController;
import com.nordstrom.automation.testng.LinkedListeners;
import com.nordstrom.automation.testng.ListenerChain;
 
import com.nordstrom.example.listeners.ScenarioCleanup;
 
@LinkedListeners({DriverManager.class, ExecutionFlowController.class})
public class ExampleTest {
  
    @Test
    public void testSomething() {
        ...
    }
     
    @AfterMethod
    public void scenarioSpecificCleanup(ITestResult testResult) {
        WebDriver driver = DriverManager.getDriver(testResult);
         
        // perform scenario-specific cleanup
        ...
    }
     
    ...
  
}

In the example above, the ScenarioCleanup listener is attached to the listener chain prior to DriverManager. Consequently, the ITestListener after methods of ScenarioCleanup are invoked prior to those of DriverManager. At this point, the driver will still be open, allowing interactions with the browser session.

The @AfterMethod configuration method in this example is also able to interact with the browser session. Depending on your scenario, you may need to add conditional logic to check the completion status of the test method, but this is easily done through the various status-related methods of the ITestResult object.

Component Container Logging

In Selenium Foundation, component containers are enhanced - additional facilities are added at runtime to improve performance and eliminate boilerplate code. The enhancement process produces a variant of the original class with an augmented name. If you use the standard technique to obtain an SLF4J Logger, it will be assigned this augmented name instead of the unadorned name you might expect:

    System.out.println(pageObj.getName());
    // => com.nordstrom.automation.selenium.model.EnhancedExamplePage

The Enhanceable class, which is the base class for all of the component container classes, includes a getContainerClass(Object) method that returns the original class. However, you never need to create you own loggers, because each container object comes equipped with a logger out of the box:

    pageObj.getLogger().info("Informative log message");
    => 21:19:12.910 [main] INFO  c.n.a.selenium.model.ExamplePage - Informative log message

As demonstrated, each component container - in this case, a Page object - has a logger created for it automatically. You access this logger by way of the getLogger() method.