A Journey to Better Automation with the Screenplay Pattern

Advanced Topics, Getting Started — March 18, 2022

In this tutorial, we’ll examine the limitations of automation with raw WebDriver calls and the Page Object Model, before learning how to use the Screenplay Pattern with Boa Constrictor to make more reliable interactions for better test automation.

I welcome you to join me on a journey through a process of automating better interactions. Grab your adventure hat and your gear and let’s get started!

You can watch the talk I gave at Future of Testing: Frameworks 2022, or you can keep reading below.

In this journey, we will explore a design pattern that is useful for automating better interactions: the Screenplay Pattern. This pattern has been around for several years, but perhaps it is still new to many people in our industry. I want to raise awareness of how useful Screenplay really is. I believe that the Screenplay Pattern offers a better approach to automation than the ways which are traditionally used.

Our route on this journey includes several main points of interest:

  1. Automation with raw WebDriver calls
  2. Automation with the Page Object Model
  3. Introduction to the Screenplay Pattern
  4. Automation with the Screenplay Pattern using Boa Constrictor

We’ll use C# for code examples. Let’s go!

Interactions

Let’s begin this adventure by exploring a fundamental building block to automation: interactions. An interaction is simply how a user operates software. Here, we will explore Web UI interactions, such as clicking buttons and scraping text. Interactions are essential for testing.

Testing is interaction plus verification. You do something and then confirm that it works. It’s simple!

Now, take a moment to consider your own experiences with functional test cases. Each one was probably a step-by-step procedure, where each step had interactions and verifications. As an example, let’s walk through a simple DuckDuckGo search test. DuckDuckGo is a search engine, like Google. The steps for it should be straightforward:

  1. Open the search engine. (This requires navigation.)
  2. Search for a phrase. (This requires entering keystrokes and clicking the search button.)
  3. Verify the results.  (This requires scraping the page title and the result links from the new page.)

A diagram for a simple DuckDuckGo search test

Look at all those interactions!

It seems that handling automated Web UI interactions well is such a challenge in our industry. Usage of the available tools for interactions, like Selenium WebDriver, can vary by team. Also, where there are Web UI interactions, you’re most certainly able to find those pesky critters known as “code duplication” and “flakiness.” Let’s explore the common way that many people learn to start on their journey of automating interactions.

Automation With Raw WebDriver Calls

As people begin to code automated tests, they often start by writing raw Selenium WebDriver calls. If you have had any previous experience with the WebDriver API, you’re probably already familiar with these kinds of calls.

The following code uses raw WebDriver calls in C# to perform the simple DuckDuckGo search test:

IWebDriver driver = new ChromeDriver();

// Open the search engine
driver.Navigate().GoToUrl("https://duckduckgo.com/");

// Search for a phrase
driver.FindElement(By.Id("search_form_input_homepage")).SendKeys("eevee");
driver.FindElement(By.Id("search_button_homepage")).Click();

// Verify results appear
driver.Title.ToLower().Should().Contain("eevee");
driver.FindElements(By.CssSelector("a.result__a")).Should().BeGreaterThan(0);

driver.Quit();

To prep the test, we first need to initialize the WebDriver object. We will use ChromeDriver for the Chrome browser. Then we need to navigate to DuckDuckGo. To do a search, we need to provide locators for the desired Web elements to send a search phrase and click search. Next, we want to verify the results of our search. To do this, we’ll make assertions on the title of the result page and its links. Before we complete our test, we need to make sure we call Quit on the WebDriver object. A good hiking rule is to leave nothing behind but footprints (or perhaps in the case of test automation, leave only logs to show that you were there).

For anyone who has traveled down this path before, perhaps you already spot the trouble up ahead in this code: race conditions! A race condition happens when automation tries to interact with a page or element before it’s fully loaded. In our test case, there are three places where the automation doesn’t properly wait. WebDriver methods aren’t equipped to wait automatically. This is a big reason why many tests are flaky: because they do not properly handle waiting.

One option would be to add an implicit wait for a target element. However, that isn’t viable in all situations, like the race condition in our assertion on the title.

Another option would be to use explicit waits. These give us more control over waiting in our test. To use them, let’s create a new WebDriverWait object named “wait” and give it a timeout value. Next, we’ll use the wait.Until() method to place the wait object before elements that need time to be ready. This method takes a function that returns true when the condition is reached.

IWebDriver driver = new ChromeDriver();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(30));

// Open the search engine
driver.Navigate().GoToUrl("https://duckduckgo.com/");

// Search for a phrase
wait.Until(d => d.FindElements(By.Id("search_form_input_homepage")).Count > 0);
driver.FindElement(By.Id("search_form_input_homepage")).SendKeys("eevee");
driver.FindElement(By.Id("search_button_homepage")).Click();

// Verify results appear
wait.Until(d => d.Title.ToLower().Contains("eevee"));
wait.Until(d => d.FindElements(By.CssSelector("a.result__a"))).Count > 0);

driver.Quit();

Look out! Adding explicit waits mitigated one problem but created more issues in the process. We can see that the search_form_input_homepage element is used multiple times. This is the “code duplication” pest. The other pest we encounter here is called “unintuitive code.” If the comments are removed, it becomes more difficult to quickly understand what the code is doing. Oh no!

IWebDriver driver = new ChromeDriver();
WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(30));
 
driver.Navigate().GoToUrl("https://duckduckgo.com/");
 
wait.Until(d => d.FindElements(By.Id("search_form_input_homepage")).Count > 0);
driver.FindElement(By.Id("search_form_input_homepage")).SendKeys("eevee");
driver.FindElement(By.Id("search_button_homepage")).Click();
 
wait.Until(d => d.Title.ToLower().Contains("eevee"));
wait.Until(d => d.FindElements(By.CssSelector("a.result__a"))).Count > 0);
 
driver.Quit();

Automation With the Page Object Model

I would hate to scale that code out any further. The Page Object Model is a method people commonly turn to at this point. Let’s try it out!

To use the Page Object Model, we’ll need to set up a class that has locator variables for elements and methods for interactions. Let’s explore this option by creating a search page class:

public class SearchPage
{
    public const string Url = "https://duckduckgo.com/";
    public static By SearchInput => By.Id("search_form_input_homepage");
    public static By SearchButton => By.Id("search_button_homepage");
   
    public IWebDriver Driver { get; private set; }
   
    public SearchPage(IWebDriver driver) => Driver = driver;
   
    public void Load() => Driver.Navigate().GoToUrl(Url);
 
    public void Search(string phrase)
    {
        WebDriverWait wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(30));
        wait.Until(d => d.FindElements(SearchInput).Count > 0);
        Driver.FindElement(SearchInput).SendKeys(phrase);
        Driver.FindElement(SearchButton).Click();
    }
}

We will start by adding some variables and giving them intuitive names. We will add a constant for the search page URL and variables to hold locators for search input and search button. Then we’ll add a variable to store a reference to the WebDriver, which will get injected through the constructor.

Now for the methods. We’ll add a load method for navigating to the search page URL. We will also add a search method. It first initializes a wait object and then uses that to wait for the search input to appear. After that, it puts in the phrase and clicks the search button to complete the search.

So far, this looks better than raw WebDriver calls. We begin to see the concerns are being separated. This code is easier to read because the different pieces have meaningful names. They are also reusable which is a step towards dealing with the “code duplication” pest.

Let’s refactor our simple search test using this new SearchPage class. We can also apply the Page Object Model to the other steps. Here is the refactored code:

IWebDriver driver = new ChromeDriver();
 
SearchPage searchPage = new SearchPage(driver);
searchPage.Load();
searchPage.Search("eevee");
 
ResultPage resultPage = new ResultPage(driver);
resultPage.WaitForTitle("eevee");
resultPage.WaitForResultLinks();
 
driver.Quit();

This looks much better.

Okay, I know I said that page objects help cut down on that pesky code duplication. However, it doesn’t deal with all of it. To show you what I mean, let’s consider interaction methods. Our test has a method to click on one button, but what happens if there is another button on the search page we might want to click? Well, we would need to add another method to click that button, such as in the following code:

public class AnyPage
{
    // ...
    public void ClickButton()
    {
        Wait.Until(d => d.FindElements(Button).Count > 0);
        driver.FindElement(Button).Click();
    }
   
    public void ClickOtherButton()
    {
        Wait.Until(d => d.FindElements(OtherButton).Count > 0);
        driver.FindElement(OtherButton).Click();
    }
}

We would use the same code for both of those click methods. On top of that, if there were more buttons to click, the same thing would happen. We may have thought we left that pest “code duplication” behind, yet here it is popping up again. It’s so pesky!

Don’t fret yet! I have heard that a “base page” pairs well with page objects. We can abstract the common parts into a central parent class and then use them for any page object we want. Maybe that will help us deal with this pest. Check it out:

public class BasePage
{
    public IWebDriver Driver { get; private set; }
    public WebDriverWait Wait { get; private set; }
   
    public SearchPage(IWebDriver driver)
    {
        Driver = driver;
        Wait = new WebDriverWait(Driver, TimeSpan.FromSeconds(30));
    }
 
    protected void Click(By locator)
    {
        Wait.Until(d => d.FindElements(locator).Count > 0);
        driver.FindElement(locator).Click();
    }
}

First, we will refactor the variables for WebDriver and wait objects into our new BasePage class. Next, we will add common interaction methods, like Click(). I’m so glad we found abstraction as a hiking stick to help us along! Any child class we would like to write can now inherit these methods. I hope you are not tired because we still have more hiking to do – our destination has not been reached yet!

The trouble with page objects is that they combine two concerns: page elements and their interactions. We would still need to write page object methods for every interaction an element might need. For example, every click, scrape, or appearance check is a new interaction. We would have to write all those interactions for other elements as well. Here is an example:

public class AnyPage : BasePage
{
    // ...
 
    public void ClickButton() => Click(Button);
    public void ClickOtherButton() => Click(OtherButton);
 
    public void ButtonText() => Text(Button);
    public void OtherButtonText() => Text(OtherButton);
   
    public void IsButtonDisplayed() => IsDisplayed(Button);
    public void IsOtherButtonDisplayed() => IsDisplayed(OtherButton);
}

Unfortunately, this issue is not mitigated by using a base page. The base page may abstract the specific interaction code, but it does not reduce the number of interactions we would need to write.

Yikes, this path looks like it could get steep!

One other frustration with page objects is that there isn’t a real structure. If you have been down the Page Object Path before, consider this: if a junior tester joined your team, would they be able to look at the code and easily determine the structure of it? If an experienced developer joined your team, would your implementation of the page object pattern look like the implementation they used previously? My guess is that the answer would be “no.” Why? Because there is no official version of the Page Object Pattern nor conformity to its design. What is enforcing a structure? Nothing that I know of. Pages objects are more of a convention rather than a design pattern.

Isn’t there a better way to handle interactions?!

Introduction to the Screenplay Pattern

Aha, the Screenplay Pattern! Maybe this will have better interactions for better automation. Come on, friends, let the adventure continue!

Let’s first examine interactions through the lens of the Screenplay pattern. For an interaction to happen, it needs an initiator. Typically, this entity is a user. The Screenplay Pattern calls this role the “Actor.” The Actor is the one who takes actions like clicking buttons or sending keys.

Interactions need something to act upon, like elements on a web page. All those pieces are part of the product that’s being tested. Our journey is using a Web app for testing, but the product is not limited to just that. It could be other things like microservices or mobile apps.

Then we have the actual interaction itself. We have already seen simple ones like clicks and scrapes. Interactions can also be more complex, like what we saw when creating a Search method for our SearchPage class. The great thing is each interaction will operate the same on whatever target is given to it.

Finally, we need objects that will allow Actors to perform interactions. The Screenplay Pattern calls these “Abilities.” In our web test, we’re using Selenium WebDriver as the tool to automate browser interactions.

Components of the Screenplay Pattern

Actor, Ability, and Interaction – these three things are the crucial blocks of the Screenplay pattern. They each represent a different concern. Their relationship can simply be explained like this:

Actors use Abilities to perform Interactions.

If you only remember one thing from today’s journey, remember this point because it is the heart of the Screenplay Pattern.

As we started to discover earlier, page objects can become difficult to untangle because of how the concerns are combined. The Screenplay Pattern uses Actors, Abilities, and Interactions to nicely separate out the concerns, which allows for greater reusability and scalability.

Automation With the Screenplay Pattern Using Boa Constrictor

Now that we have been introduced to this pattern, let’s explore Screenplay further using Boa Constrictor.

Boa Constrictor is an open-source C# implementation of the Screenplay Pattern. It was developed under the lead of Andy Knight at PrecisionLender as the cornerstone of PrecisionLender’s end-to-end test automation solution. The project can be used with any .NET test framework, like SpecFlow or NUnit.

Boa Constrictor – The .NET Screenplay Pattern
Boa Constrictor – The .NET Screenplay Pattern

We will use Boa Constrictor to further evolve our simple DuckDuckGo search test. All the example code we are about to explore is copied directly from the Boa Constrictor GitHub repository with permission.

Setup

This demo requires installing the following NuGet packages and declaring dependencies:

// NuGet Packages:
//  Boa.Constrictor
//  FluentAssertions
//  Selenium.Support
//  Selenium.WebDriver
 
using Boa.Constrictor.Logging;
using Boa.Constrictor.Screenplay;
using Boa.Constrictor.WebDriver;
using FluentAssertions;
using OpenQA.Selenium.Chrome;
using static Boa.Constrictor.WebDriver.WebLocator;

Every Screenplay call begins with an Actor. The Actor’s job is to perform interactions. Typically, only a single Actor is required for most test cases. In our new test using Boa Constrictor, let’s initialize our Actor:

IActor actor = new Actor(name: "Sarah", logger: new ConsoleLogger());

The Actor class has two optional arguments. The first one is for naming the Actor to help describe who is acting. This is recorded in logged messages. The second one is for a logger, which logs messages from Screenplay calls to a target destination. The logger is required to implement Boa Constrictor’s ILogger interface. In this example, we are using the ConsoleLogger class, which logs messages to the system console. You can choose to define a custom logger instead; just make sure to implement ILogger.

Actors need Abilities to perform interactions. In this test, our Actor must have a Selenium WebDriver instance to click elements on a Web page, so we’ll give her the BrowseTheWeb Ability by using a method called Can():

actor.Can(BrowseTheWeb.With(new ChromeDriver()));

In plain English this line says, “The actor can browse the Web with a new ChromeDriver.” We can see that Boa Constrictor’s fluent-like syntax makes its call chains easy to understand.

BrowseTheWeb is an Ability that allows an Actor to initiate Web UI Interactions:

public class BrowseTheWeb : IAbility
{
    public IWebDriver WebDriver { get; }
 
    private BrowseTheWeb(IWebDriver driver) =>
        WebDriver = driver;
       
    public static BrowseTheWeb With(IWebDriver driver) =>
        new BrowseTheWeb(driver);
}

The With() method supplies a WebDriver object to the Actor, which can be retrieved from the Actor by any Web UI Interaction. Boa Constrictor supports all browser types.

Every Ability needs to implement the IAbility interface. There is no limit to the number of Abilities an Actor can have.

Unlike the Page Object Model, the Screenplay Pattern requires separating page structure concerns from interaction concerns. This structure provides greater reusability because any element can be targeted by any interaction. To do that, we write models for the Web pages we want to test. These are needed so the Actor can call WebDriver interactions. These types of models should be static classes that contain element locators for the page, and they can also include URLs. Interaction logic does not belong in page classes; they should only be used to model structure. By following the Screenplay Pattern, we now have classes of elements that can be shared among any interaction. We no longer need to write a click or a scrape for each different element.

We’ll add two members in the SearchPage class:

public static class SearchPage
{
    public const string Url =
        "https://www.duckduckgo.com/";
   
    public static IWebLocator SearchInput => L(
        "DuckDuckGo Search Input",
        By.Id("search_form_input_homepage"));
}

For convenience, locators can be constructed using the statically imported L method. There are two parts to a locator. The first is a plain-English description that will be used by the logger. The second is a Query used to find the element. Boa Constrictor uses Selenium WebDriver’s By queries.

Screenplay Interaction: Tasks

There are two types of interactions in The Screenplay Pattern: Tasks and Questions. We’ll explore a Task first. A Task is simply an action that does not return a value, like a click or a refresh.

In Boa Constrictor, there is a Task called Navigate, which is used to load a Web page for a given URL. Let’s add the following line to our test:

actor.AttemptsTo(Navigate.ToUrl(SearchPage.Url));

In plain English this line says, “The actor attempts to navigate to the URL for the search page.” We can easily see that this line will load a search page.

Every Task needs to implement the ITask interface. AttemptsTo() calls a Task. When the Actor calls AttemptsTo() on a Task, it calls the Task’s PerformAs() method:

public void AttemptsTo(ITask task)
{
    task.PerformAs(this);
}

Here is the Navigate Task:

public class Navigate : ITask
{
    private string Url { get; set; }
 
    private Navigate(string url) => Url = url;
   
    public static Navigate ToUrl(string url) => new Navigate(url);
 
    public void PerformAs(IActor actor)
    {
        var driver = actor.Using<BrowseTheWeb>().WebDriver;
        driver.Navigate().GoToUrl(Url);
    }
}

ToUrl() gives the specified URL. The Navigate Task’s PerformAs() method retrieves the WebDriver object from the Actor’s BrowseTheWeb Ability and then uses it to load the specified URL.

The SearchPage.Url parameter we gave ToUrl() in our test is from the SearchPage class. Like I said before, since it is in a page class, this URL is available to any interaction.

Screenplay Interaction: Questions

Now we will move on to explore Question Interactions. A Question can perform one or more actions and then return an answer, like scraping an element’s text or waiting for its appearance.

In Boa Constrictor, there is a Question called ValueAttribute, which is used to get the current value within a given input field. We will add it to our test:

actor.AskingFor(ValueAttribute.Of(SearchPage.SearchInput)).Should().BeEmpty();

In plain English this line says, “The actor asking for the value attribute of the search page’s search input element should be empty.”

Every Question needs to implement the IQuestion interface. AskingFor() calls a Question. There is also an equivalent method called AsksFor(). When the Actor calls either of these, it calls the Question’s RequestAs() method:

public TAnswer AskingFor<TAnswer>(IQuestion<TAnswer> question)
{
    return question.RequestAs(this);
}

Here is the ValueAttribute Question:

public class ValueAttribute : IQuestion<string>
{
    public IWebLocator Locator { get; }
   
    private ValueAttribute(IWebLocator locator) => Locator = locator;
   
    public static ValueAttribute Of(IWebLocator locator) => new ValueAttribute(locator);
 
    public string RequestAs(IActor actor)
    {
        var driver = actor.Using<BrowseTheWeb>().WebDriver;
        actor.AttemptsTo(Wait.Until(Existence.Of(Locator), IsEqualTo.True()));
        return driver.FindElement(Locator.Query).GetAttribute("value");
    }
}

Of() gives the specified Web element’s locator. The ValueAttribute Question’s RequestAs() method retrieves the WebDriver object, waits for existence on the page of the specified element, then scrapes and returns its value attribute.

The SearchPage.SearchInput parameter we gave Of() in our test is from the SearchPage class. It is the locator for the search input field.

Now that we have a value, our test can make assertions on it. Should().BeEmpty() is a Fluent Assertion that verifies if the search input field is empty after the page is first loaded.

Screenplay Interaction: Custom Interactions

Let’s explore creating custom interactions in this next step. There are two basic interactions involved when doing a search. The first is entering the phrase in the search input and the second is clicking the search button. It makes sense to create a custom interaction for this since searching is so common. We can do that by combining lower-level interactions.

We’ll call our custom Task “SearchDuckDuckGo” and give it a search phrase as an argument:

public class SearchDuckDuckGo : ITask
{
    public string Phrase { get; }
 
    private SearchDuckDuckGo(string phrase) =>
        Phrase = phrase;
   
    public static SearchDuckDuckGo For(string phrase) =>
        new SearchDuckDuckGo(phrase);
   
    public void PerformAs(IActor actor)
    {
        actor.AttemptsTo(SendKeys.To(SearchPage.SearchInput, Phrase));
        actor.AttemptsTo(Click.On(SearchPage.SearchButton));
    }
}

The two interactions it should call in its PerformAs() method are SendKeys() and Click().

You can see that we made the code more understandable by combining both interactions into a custom one. Another benefit is the ability to reuse this automation.

actor.AttemptsTo(SearchDuckDuckGo.For("eevee"));

In plain English this line is now short, sweet and to the point: “The actor attempts to search DuckDuckGo for eevee.”

Now we’re ready for our final assertion – verifying that the result links have appeared. We know from the previous versions of this test that this step contains a race condition: we still need to wait for the links to be displayed after the page loads. If the automation checks too early, the test case will fail. Do not despair my adventure companions, waiting can be easy when using Boa Constrictor!

actor.WaitsUntil(Appearance.Of(ResultPage.ResultLinks), IsEqualTo.True());

In plain English this line says, “The actor waits until the appearance of result page result links is equal to true.”

WaitsUntil() is a Boa Constrictor method that will call a Question repeatedly until the answer meets a specified condition or it reaches the timeout. The Question here is asking for the appearance of result links on the result page.

public static IWebLocator ResultLinks => L(
    "DuckDuckGo Result Page Links",
    By.ClassName("result__a"));

The waiting condition IsEqualTo.True() is waiting for the answer value to become “true.” This Question will return “false” before the links are loaded. It will return “true” after the links appear. Boa Constrictor comes with several conditions readily available. A few examples are equality and string matching. Custom conditions can be created by implementing the ICondition interface.

Asking a Question repeatedly until the answer is met is better than hard sleeps. Upon failure to receive the expected answer within a given timeout, an exception is raised. The good news is that waiting is already taken care of in many of Boa Constrictor WebDriver interactions! If it has a target element, the interaction will wait for the element’s existence before taking further action. We have already seen this when we used Click() and SendKeys(). Be mindful when using interactions that ask for appearance or existence, as those do not have waiting built in.

We’re almost done! This last part is important – don’t forget to quit the browser. We can do this by using Boa Constrictor’s QuitWebDriver() Task.

actor.AttemptsTo(QuitWebDriver.ForBrowser());

Regardless of what framework you use, the best practice is to put this in a cleanup or teardown routine. That way, even if a test fails, the browser is still cleaned up. Remember, my adventure buddies, leave nothing but footprints.

Congratulations, we have reached this journey’s destination! Here we have a complete test case using the Screenplay Pattern with Boa Constrictor. It’s easy to understand, smartly handles race conditions, and separates concerns for better interactions.

IActor actor = new Actor(name: "Sarah", logger: new ConsoleLogger());

actor.Can(BrowseTheWeb.With(new ChromeDriver()));

actor.AttemptsTo(Navigate.ToUrl(SearchPage.Url));

actor.AskingFor(ValueAttribute.Of(SearchPage.SearchInput)).Should().BeEmpty();

actor.AttemptsTo(SearchDuckDuckGo.For("eevee"));

actor.WaitsUntil(Appearance.Of(ResultPage.ResultLinks), IsEqualTo.True());

actor.AttemptsTo(QuitWebDriver.ForBrowser());

Conclusion

Remember, the Screenplay Pattern is summarized in this statement: Actors use Abilities to perform Interactions. Simple.

I will sum up what we’ve discovered in five main points.

  1. The Screenplay Pattern offers rich, reusable, and reliable interactions. Specifically, Boa Constrictor comes with built-in Tasks and Questions for every type of WebDriver-based interactions.
  2. Screenplay interactions are composable. It is easy to combine interactions, which alleviates the issues caused by code that is difficult to understand and full of duplication.
  3. The Screenplay Pattern makes waiting easy using existing Questions and conditions. One of the most challenging parts of black box automation is the proper handling of waiting.
  4. Screenplay calls are understandable. Their fluent-like syntax reads more like prose than code.
  5. The Screenplay Pattern is a design pattern for any type of interaction, not just for Web UI. On this journey, we discovered how to use it for Web UI interactions, but it is also an option that can be applied to mobile, REST API, and other things. On top of that, you can even design your own interactions.

To support this, I would like to share a bit of my personal experience learning it. I was introduced to this pattern a little over a year ago. After I learned how to use Screenplay, I found that writing new automation with Boa Constrictor was intuitive. I also enjoyed how easy it was to understand the code, basically like plain English, which made debugging issues that much easier. In a short amount of time it helped me gain confidence in writing new tests and understanding my way around the test automation solution. It also provided a great structure for me to develop within. I’m proud to say the solution boasts over 2200 unique Web UI test cases and is still scaling well. We even published a case study with Specflow on it!

The Screenplay Pattern provides better interactions for better automation. Screenplay is a fantastic option for automating behaviors under test. As we already discovered, the Screenplay Pattern is simple. Actors use Abilities to perform Interactions. That’s all there is to it.

The journey does not have to end here. I invite you to check out the Boa Constrictor GitHub repository and to try the tutorials for yourself from the doc site.

I hope everyone had a fun and informative journey today (and that no one got stuck back there on the path of Page Object Models.) Thanks for being my journey companions today. Happy automating!

Are you ready?

Get started Schedule a demo