Tutorial Part 2 - Web UI Testing

In this part of the tutorial, you will use Boa Constrictor to automate a Web UI test to search Wikipedia for an article. The test case steps will be simple:

  1. Load the Wikipedia main page
  2. Verify the search field is empty
  3. Enter a search phrase
  4. Verify the desired article is displayed

WebDriver: Boa Constrictor’s Web UI interactions use Selenium WebDriver.

Prerequisite: Make sure you created the Boa.Constrictor.Example tutorial project from Tutorial Part 1 - Setup before attempting Part 2.

Screenplay Basics

The Screenplayer Pattern is a design pattern for automating interactions with features under test. Boa Constrictor is a C# implementation of this pattern. Rather than giving a bunch of definitions and diagrams up front, this tutorial will show how to use Boa Constrictor’s Screenplay Pattern step-by-step with example code.

The heart of the Screenplay Pattern can be defined in one line: Actors use Abilities to perform Interactions.

  • Actors initiate Interactions.
  • Abilities enable Actors to initiate Interactions.
  • Interactions are procedures that exercise behaviors under test.

The following steps will explain each component in detail.

1. Creating a Test Class

Inside the Boa.Constrictor.Example project, create a new file named ScreenplayWebUiTest.cs. Add the following NUnit code stub to the file:

using NUnit.Framework;

namespace Boa.Constrictor.Example
{
    public class ScreenplayWebUiTest
    {
        [Test]
        public void TestWikipediaSearch()
        {

        }
    }
}

ScreenplayWebUiTest will contain the main NUnit test case for Part 2 of this tutorial. To make sure it works, build the project and run the test. You can run the test from Test Explorer in Visual Studio or from the command line using the NUnit Console Runner. The test should run and pass.

2. Creating the Actor

The Actor is the entity that initiates Interactions. It represents the main caller. For example, it could represent a user logged into a Web app. All Screenplay calls start with an Actor. Most test cases need only one Actor.

To create an actor, add the following import statements to ScreenplayWebUiTest:

using Boa.Constrictor.Screenplay;

Then, add the following constructor call to its TestWikipediaSearch method:

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

Actors implement the IActor interface, which is part of the Boa.Constrictor.Screenplay namespace. The Actor class optionally takes two arguments:

Argument Purpose
name Helps describe who the actor is. The default name is “Screenplayer”. The name will appear in logged messages.
logger Send log messages from Screenplay calls to a target destination.

Loggers must implement the ILogger interface. They are part of the Boa.Constrictor.Screenplay namespace. ConsoleLogger is a class that will log messages to the system console. You can define your own custom loggers by implementing ILogger. You can also combine multiple loggers together using TeeLogger.

Build and run the test. It should pass.

3. Adding Web UI Abilities

Abilities enable Actors to initiate Interactions. That might sound a little weird at first. In a programming sense, Abilities provide objects that Actors use when calling Interactions. For example, an Actor needs a Selenium WebDriver instance in order to click elements on a Web page.

Add the following imports to ScreenplayWebUiTest:

using Boa.Constrictor.Selenium;
using OpenQA.Selenium.Chrome;

Then, add the following Ability to the Actor:

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

Read this line in plain English: “The actor can browse the Web with a new ChromeDriver.” Boa Constrictor’s fluent-like syntax makes its call chains very readable. Let’s unpack what this line does:

Code Purpose
new ChromeDriver() Web UI automation uses Selenium WebDriver to control a browser. This code instantiates a new WebDriver object for ChromeDriver.
BrowseTheWeb An Ability that enables Actors to perform Web UI Interactions.
BrowseTheWeb.With(...) Constructs the Ability with the given WebDriver object.
actor.Can(...) Adds the given Ability to the Actor. In this case, the actor Actor is given the BrowseTheWeb Ability with a ChromeDriver object.

Other Browsers: You could use a different browser type here, and you could also specify WebDriver options.

Headless Mode: You could use headless Chrome by setting options like this:

ChromeOptions options = new ChromeOptions();
options.AddArgument("headless");
options.AddArgument("window-size=1920,1080");
ChromeDriver driver = new ChromeDriver(options);

Abilities must implement the IAbility interface:

public interface IAbility
{
}

IAbility does not have any required methods. It simply provides a type system for Abilities. You can implement your own Abilities using this interface.

The BrowseTheWeb Ability is part of the Boa.Constrictor.Selenium namespace. It looks like this:

public class BrowseTheWeb : IAbility
{
    public IWebDriver WebDriver { get; }

    private BrowseTheWeb(IWebDriver driver) =>
        WebDriver = driver;

    public static BrowseTheWeb With(IWebDriver driver) =>
        new BrowseTheWeb(driver);
}

It simply holds a reference to the WebDriver object. Web UI Interactions will retrieve this WebDriver object from the Actor. Thus, Abilities act as a form of dependency injection for Interactions.

Multiple Abilities: Actors can be given any number of Abilities. For example, one Actor can have both BrowseTheWeb and CallRestApi (from the Boa.Constrictor.RestSharp namespace). For best practice, give Actors only the Abilities they need.

Build and run the test again. This time, the test should open a Chrome browser window and then stop. This means the Ability is working! Close the browser window when the test stops.

4. Modeling Web Pages

Before the Actor can call any WebDriver-based Interactions, the Web pages under test need models. These models should be static classes that include locators for elements on the page and possibly page URLs. Page classes should only model structure - they should not include any interaction logic.

The Screenplay Pattern separates the concerns of page structure from interactions. That way, interactions can target any element, maximizing code reusability. Interactions like clicks and scrapes work the same regardless of the target elements. (This is different from the Page Object Model, in which page object classes place locators together with interaction methods.)

The Wikipedia search test interacts with two pages: the “search” page (or the “home” page) and the “result” page. Each page should have its own class.

Create a file named MainPage.cs and add the following code:

using Boa.Constrictor.Selenium;
using OpenQA.Selenium;
using static Boa.Constrictor.Selenium.WebLocator;

namespace Boa.Constrictor.Example
{
    public static class MainPage
    {
        public const string Url = "https://en.wikipedia.org/wiki/Main_Page";

        public static IWebLocator SearchInput => L(
          "Wikipedia Search Input",
          By.Name("search"));
    }
}

The MainPage class has two members. The first member is a URL string named Url. Sometimes, it is convenient to hard-code URLs for pages.

The second member is a locator for the search input element named SearchInput. A locator is a pointer to a Web page element. Boa Constrictor locator objects must implement the IWebLocator interface:

public interface IWebLocator
{
    string Description { get; }
    By Query { get; }
}

A locator has two properties:

Code Purpose
Description Describes the element in plain language. It will be used for logging.
Query Finds the element on the page. Boa Constrictor uses Selenium WebDriver’s By queries. Learn more about locator queries by reading Web Element Locators for Test Automation.

For convenience, locators can be constructed using the Boa.Constrictor.Selenium.WebLocator.L static builder method. Since MainPage uses a static import for this method, it can use the short L method call.

Furthermore, notice that SearchInput uses the => operator instead of the = operator for defining the locator. The => operator makes SearchInput a read-only property: its value cannot be changed. Locators should be treated as immutable.

Boa Constrictor Interactions uses locators to interact with elements on a Web page. Interactions always fetch “fresh” element objects using locators instead of caching element objects. Fresh fetches avoid stale element exceptions.

In addition to MainPage.cs, create a file named ArticlePage.cs with the following code:

using Boa.Constrictor.Selenium;
using OpenQA.Selenium;
using static Boa.Constrictor.Selenium.WebLocator;

namespace Boa.Constrictor.Example
{
    public static class ArticlePage
    {
    }
}

Leave ArticlePage empty for now. You will add locators to both classes later in this tutorial.

5. Attempting a Task

The Screenplay Pattern has two types of Interactions. The first type of Interaction is called a Task. A Task performs actions without returning a value. Examples of Tasks include clicking an element, refreshing the browser, and loading a page. These interactions all “do” something rather than “get” something.

The test case’s first step should load the Wikipedia main page. Boa Constrictor provides a Task named Navigate under the Boa.Constrictor.Selenium namespace for loading a Web page using a target URL.

Add this line to TestWikipediaSearch:

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

Read this line in plain English: “The actor attempts to navigate to the URL for the main page.” Again, Boa Constrictor’s fluent-like syntax is very readable. Clearly, this line will load the Wikipedia search page. Let’s unpack it:

Code Purpose
MainPage.Url The target URL. It is a member of the MainPage model class so that it can be used by any Interaction.
Navigate.ToUrl(...) Constructs a Task object using the given URL string. The Navigate class provides the logic for performing the page load.
actor.AttemptsTo(...) Calls the given Task. The call is an “attempt” because the Task may or may not ultimately be successful.

All Interactions, including Tasks, must implement the IInteraction interface for common typing:

public interface IInteraction
{
}

Tasks must implement the ITask interface:

public interface ITask : IInteraction
{
    void PerformAs(IActor actor);
}

A Task’s main logic is in its PerformAs method. The method’s return type is void because Tasks don’t return anything.

Boa Constrictor provides several WebDriver-based Interactions under the Boa.Constrictor.Selenium namespace. The Navigate Task is one of them. You do not need to create it in the tutorial project. Below is a simplified version of the Navigate Task’s code:

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);
    }
}

The Navigate Task has a property named Url for its target URL. Its constructor is private so that callers must use the static Navigate.ToUrl(...) builder method, which makes calls more readable. Its PerformAs method gets a reference to the WebDriver object by “using” the Actor’s Ability to “browse the Web,” and then it makes WebDriver calls to navigate to the target URL.

Actors can call any Tasks using the AttemptsTo method:

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

The AttemptsTo method takes in a Task and calls the Task’s PerformAs method. It also injects a reference to the Actor so that the Task can access the Actor’s Abilities. This pattern preserves the separation of concerns between Actors and Interactions. The Actor can call any Tasks as long as it has the appropriate Abilities. Actor code does not need to be modified to call more types of Tasks.

Build and run the test again. This time, the browser should load the Wikipedia main page. Close the browser once the test stops.

6. Asking a Question

The second type of Interaction is called a Question. A Question returns an answer after performing actions. Examples of Questions include getting an element’s text, location, and appearance. Each of these interactions return some sort of value.

The test case’s second step verifies that the search field is empty. The ValueAttribute Question gets the “value” of the text currently inside an input field. (Note: this is different from regular element text, which uses the Text Question.) To use ValueAttribute, add the following line to TestWikipediaSearch:

string text = actor.AsksFor(ValueAttribute.Of(MainPage.SearchInput));

Read this line in plain English: “The actor asks for the value attribute of the search page’s search input element.” Let’s break it down:

Code Purpose
MainPage.SearchInput The locator for the search input field. You previously added this locator to the MainPage class.
ValueAttribute.Of(...) Constructs a Question using the given locator. It returns the “value” attribute of the locator’s target element.
actor.AsksFor(...) Calls the given Question. The Actor “asks for” the answer to the Question.

Questions must implement the IQuestion interface:

public interface IQuestion<TAnswer> : IInteraction
{
    TAnswer RequestAs(IActor actor);
}

Questions are generic in their return value type. The main logic is in the RequestAs method, which returns a type-appropriate answer.

The ValueAttribute Question is one of several WebDriver-based Questions available under the Boa.Constrictor.Selenium namespace. Below is a simplified version of its code:

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.WaitsUntil(Existence.Of(Locator), IsEqualTo.True());
        return driver.FindElement(Locator.Query).GetAttribute("value");
    }
}

The ValueAttribute Question has a property for the target element’s locator named Locator. Just like the Navigate Task, it has a private constructor and a public static builder method. The RequestAs method gets the WebDriver object from the Actor’s BrowseTheWeb Ability. It then waits for the element to exist on the page, finds the element, and return’s the element’s “value” attribute. Under the hood, these are all just Selenium WebDriver calls.

Waiting: Waiting, as shown with actor.WaitsUntil(...), will be explained later in the tutorial.

Actors can call any Questions using the AsksFor method:

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

This method is analogous to AttemptsTo for Tasks.

Simply getting the search field’s value is not sufficient for testing. The test case must also verify that the value is empty using an assertion. The recommended assertion library to use with Boa Constrictor is Fluent Assertions.

Add the following import statement to ScreenplayWebUiTest:

using FluentAssertions;

Then, update the Question call like this:

string text = actor.AsksFor(ValueAttribute.Of(MainPage.SearchInput));
text.Should().BeEmpty();

You can also shorten this call to one line:

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

The AskingFor method is simply an alias for AsksFor. It improves readability when using Fluent Assertions.

Build and run the test again. It should open the browser, load Wikipedia, and pass just like before. If you want to make sure the assertion is really working, you can temporarily change it to Should().NotBeEmpty() and watch the test fail.

7. Composing a Custom Interaction

The test case’s next step is to enter a search phrase. Doing this requires two interactions: typing the phrase into the search input and clicking the search button.

Add a new locator for the search button to MainPage:

public static IWebLocator SearchButton => L(
    "Wikipedia Search Button",
    By.XPath("//button[text()='Search']"));

Then, add the following lines to TestWikipediaSearch:

actor.AttemptsTo(SendKeys.To(MainPage.SearchInput, "Giand panda"));
actor.AttemptsTo(Click.On(MainPage.SearchButton));

SendKeys and Click are two more Tasks provided by Boa Constrictor. They do precisely what they say. However, these two Interactions truly represent one larger interaction: entering a search phrase. The Screenplay Pattern makes it possible to easily compose multiple Interactions together into one new Interaction. Composition improves readability and reusability.

Create a new file named SearchWikipedia.cs and add the following code:

using Boa.Constrictor.Screenplay;
using Boa.Constrictor.Selenium;

namespace Boa.Constrictor.Example
{
    public class SearchWikipedia : ITask
    {
        public string Phrase { get; }

        private SearchWikipedia(string phrase) =>
          Phrase = phrase;

        public static SearchWikipedia For(string phrase) =>
          new SearchWikipedia(phrase);

        public void PerformAs(IActor actor)
        {
            actor.AttemptsTo(SendKeys.To(MainPage.SearchInput, Phrase));
            actor.AttemptsTo(Click.On(MainPage.SearchButton));
        }
    }
}

SearchWikipedia is a new Task that takes in a search phrase and internally calls SendKeys and Click to enter the phrase on the Wikipedia main page.

Replace the old calls in TestWikipediaSearch with this new Task:

actor.AttemptsTo(SearchWikipedia.For("Giant panda"));

Read this line in plain English: “The actor attempts to search Wikipedia for ‘Giant panda’.” This call is much more intuitively understandable than the previous calls. It conveys intention: the purpose of the step is not to send arbitrary keystrokes and clicks but rather to perform a Wikipedia search.

Custom Interactions like SearchWikipedia add a little more code at first, but they can ultimately avoid lots of repetitive code. You should make custom Interactions for common operations shared by multiple tests. For example, SearchWikipedia would be very useful for additional search tests.

Build and run the test again. This time, you should see the search happen.

8. Waiting for Questions to Yield Answers

The last test case step should verify that the desired article appears entering a search phrase. Unfortunately, this step has a race condition: the article takes a few seconds to load. Automation must wait for the page to appear. Checking too early will make the test case fail.

Boa Constrictor makes waiting easy. Add the following locator to the ArticlePage class:

public static IWebLocator Title => L(
    "Title Span",
    By.CssSelector("[id='firstHeading'] span"));

This locator will find the title heading on the article page.

Then, add the following line to TestWikipediaSearch:

Actor.WaitsUntil(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda"));

Read this line in plain English: “The actor waits until the text of the article page title is equal to the value ‘Giant panda’.” In simpler terms, “Wait until the article title is ‘Giant panda’.” Let’s break it down:

Code Purpose
ArticlePage.Title The locator for the article’s title.
Text.Of(...) A Question that returns the text value of the target element.
IsEqualTo.Value(...) A Condition for checking if the return value of a Question equals a given value.
Actor.WaitsUntil(...) An extension method that halts execution until the given Question’s answer meets the given Condition. In this case, the appearance of the result links must become true.

WaitsUntil is an IActor extension method that internally calls waiting interactions. The following calls are essentially the same:

// The full, "traditional" way to wait
actor.AttemptsTo(Wait.Until(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda")));

// The more concise way to wait
Actor.WaitsUntil(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda"));

There are two waiting interactions under the Boa.Constrictor.Screenplay namespace: a Task named Wait and a Question named ValueAfterWaiting. Both waiting interactions work for any type of Question, not just WebDriver-based Questions. If the given Question fails to meet the given Condition within a timeout limit, then waiting throws a WaitingException. The default timeout is 30 seconds, but it may be overridden like this: WaitsUntil(..., timeout: 60), or like this: Wait.Until(...).ForUpTo(60).

Waiting also requires Conditions. A Condition is a required state for a value. Boa Constrictor provides several basic conditions under the Boa.Constrictor.Screenplay namespace, such as IsNot, IsLessThan, IsGreaterThan, and Matches. All conditions must implement the ICondition interface:

public interface ICondition<TValue>
{
    bool Evaluate(TValue actual);
}

The Wait Task will repeatedly call its given Question and pass the answer into its given Condition’s Evaluate method until Evaluate returns true or the elapsed waiting time exceeds the timeout limit.

Many WebDriver-based Interactions do waiting internally. For example, the ValueAttribute Question shown in a previous step waits for the existence of the target element before getting its “value” attribute. Automatic waiting is a major advantage of Boa Constrictor’s Interactions. Raw WebDriver calls do not wait, and they cause race conditions (resulting in “flakiness”) when testers don’t remember to add waiting. Typically Boa Constrictor Interactions that perform an action on an element wait for the target element’s existence or appearance, and Interactions that check existence (like Appearance, Existence, and Count) do not do waiting.

Since this test case step simply needs to verify that the result links appeared, it does not need to make an explicit assertion. The Wait Task has an implicit assertion in that failure to meet the Condition throws an exception.

Build and run the test again. The browser should do the same things as before, and the test case should pass. This time, the test won’t stop until the result links appear.

9. Quitting the Browser

Before ending the test case, the browser must be quit safely. Otherwise, the browser and its associated WebDriver executable will keep running, hogging system resources and possibly causing other problems.

Add the following call to the bottom of TestWikipediaSearch:

actor.AttemptsTo(QuitWebDriver.ForBrowser());

Read this line in plain English: “The actor attempts to quit the WebDriver for the browser.” Internally, this Task calls the WebDriver’s Quit method.

Build and run the test again. This time, when the test finishes, it will automatically quit the browser window.

10. Refactoring the Project

The test steps are complete, and if you run the test case, it should pass. However, it should be refactored a bit for better setup and cleanup. Rewrite ScreenplayWebUiTest with the following code:

using Boa.Constrictor.Screenplay;
using Boa.Constrictor.Selenium;
using FluentAssertions;
using NUnit.Framework;
using OpenQA.Selenium.Chrome;

namespace Boa.Constrictor.Example
{
    public class ScreenplayWebUiTest
    {
        private IActor Actor;

        [SetUp]
        public void InitializeScreenplay()
        {
            ChromeOptions options = new ChromeOptions();
            options.AddArgument("headless");                  // Remove this line to "see" the browser run
            options.AddArgument("window-size=1920,1080");     // Use this option with headless mode
            ChromeDriver driver = new ChromeDriver(options);

            Actor = new Actor(name: "Andy", logger: new ConsoleLogger());
            Actor.Can(BrowseTheWeb.With(driver));
        }

        [TearDown]
        public void QuitBrowser()
        {
            Actor.AttemptsTo(QuitWebDriver.ForBrowser());
        }

        [Test]
        public void TestWikipediaSearch()
        {
            Actor.AttemptsTo(Navigate.ToUrl(MainPage.Url));
            Actor.AskingFor(ValueAttribute.Of(MainPage.SearchInput)).Should().BeEmpty();
            Actor.AttemptsTo(SearchWikipedia.For("Giant panda"));
            Actor.WaitsUntil(Text.Of(ArticlePage.Title), IsEqualTo.Value("Giant panda"));
        }
    }
}

Actor and Ability creation are part of the SetUp method because they could be shared by multiple tests. The QuitWebDriver Task is part of the TearDown method so that every test quits the browser even upon failure. Never forget to do that!

Furthermore, source files should be organized by concern. Create new folders and move source files like this:

Boa.Constrictor.Example
│
├── Interactions
│   └── SearchWikipedia.cs
│
├── Pages
│   ├── ArticlePage.cs
│   └── MainPage.cs
│
└── Tests
    └── ScreenplayWebUiTest.cs

Build and run the test code one final time to make sure it passes.

Conclusion

Congrats on finishing Part 2 of the tutorial!

Boa Constrictor provides several Web UI Interactions that could not be covered in this brief tutorial. All interactions using Selenium WebDriver are located under Boa.Constrictor\WebDriver. Take some time to review them. They will be very useful when writing new tests. You can also use them as examples for writing new Interactions.

Proceed to Part 3 - REST API Testing to learn how to use Boa Constrictor’s REST API interactions.