Tutorials · Lesson 5 of 6 · about 30 minutes

Start from lesson 4. You need the project from More controls, with its tests passing, or git checkout lesson-4 in VAF-Tutorial.

Your project repeats itself. Every test class opens and closes the browser in its own way, and filling in the sign-up form is written out wherever a test needs it. When the form changes, you would change every one of those places. In this lesson you move the browser into one class, the page context into another, and the actions of the user into shared steps — and the tests shrink to a few lines that read like what the user does.

1

A shared context

Add a folder Common and in it SharedContext.cs. It holds what every test and every step needs: the browser and the page context, App.

FirstVafTest/Common/SharedContext.cs On GitHub
using FirstVafTest.PageObjects;
using VisionAutomationFramework;
using VisionAutomationFramework.Actions;
using VisionAutomationFramework.Browsers;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.Common
{
    // What every test and every step needs: the browser and the page context of the application.
    // TestBase starts and closes the browser; a test or a step opens its page with LetsOpen, and
    // every chain starts from App. The properties are static, so the tests run one after another.
    public static class SharedContext
    {
        public const string Site = "https://www.visionautomationframework.com";

        public static BrowserActions Browser { get; private set; } = null!;
        public static PageActions<AppPage> App { get; private set; } = null!;

        public static void LetsStartTheBrowser() => Browser = Testing.On<Chrome>();

        // Goes to a page of the site, e.g. "/practice/forms", and fills App
        public static void LetsOpen(string path)
        {
            App = Browser.LetsNavigateTo(Site + path)
                .Should().DisplayPage<AppPage>().That;
        }

        public static void LetsCloseTheBrowser()
        {
            Browser?.Scope.Driver.Quit();
            Browser = null!;
            App = null!;
        }
    }
}

LetsOpen takes the path of a page, so the address of the site is written once. The properties are static: every step can reach them without passing anything around. The price is that the tests run one after another, not in parallel — which is how NUnit runs them anyway, unless you ask for more.

2

A test base

Add TestBase.cs to Common:

FirstVafTest/Common/TestBase.cs On GitHub
using NUnit.Framework;

namespace FirstVafTest.Common
{
    // The base of every test class: the browser starts before each test and closes after it.
    // A test class adds only its own [SetUp], usually one that opens its page; NUnit runs this
    // [SetUp] first, then the one of the test class.
    public abstract class TestBase
    {
        [SetUp]
        public void StartTheBrowser() => SharedContext.LetsStartTheBrowser();

        [TearDown]
        public void CloseTheBrowser() => SharedContext.LetsCloseTheBrowser();
    }
}

Every test class will derive from it, so the browser is started and closed in exactly one place. When a test class has its own [SetUp], NUnit runs the one of TestBase first — the browser is there before the test class opens its page. Lesson 6 adds a report and a screenshot here, and every test gets them.

3

Shared steps

Add a folder SharedSteps and in it FormsSteps.cs. Each step is a static method named after what the user does, and holds the chain that does it:

FirstVafTest/SharedSteps/FormsSteps.cs On GitHub
using System;
using FirstVafTest.Common;
using FirstVafTest.PageObjects;
using VisionAutomationFramework.Core;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.SharedSteps
{
    // What a user does on /practice/forms, one step per action. Tests call these steps and do not
    // know how the form is filled in: when the form changes, only this file changes.
    public static class FormsSteps
    {
        public static void LetsOpenTheForm() => SharedContext.LetsOpen("/practice/forms");

        // plan is Free, Team or Enterprise
        public static void LetsRegister(string fullName, string email, string password, string country, string plan = "Free")
        {
            SharedContext.App.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.FullName>()
                    .That.AsTextInputField().LetsInsert(fullName)
                .AndAlso().Should().HaveElement<FormsPage.Registration.Email>()
                    .That.AsTextInputField().LetsInsert(email)
                .AndAlso().Should().HaveElement<FormsPage.Registration.Password>()
                    .That.AsTextInputField().LetsInsert(password)
                .AndAlso().Should().HaveElement<FormsPage.Registration.Country>()
                    .That.AsSelectBox().LetsSelect(country);

            // Each plan is its own radio button; the test only names the plan
            switch (plan)
            {
                case "Free": LetsChooseThePlan<FormsPage.Registration.PlanFree>(); break;
                case "Team": LetsChooseThePlan<FormsPage.Registration.PlanTeam>(); break;
                case "Enterprise": LetsChooseThePlan<FormsPage.Registration.PlanEnterprise>(); break;
                default: throw new ArgumentException("No such plan: " + plan, nameof(plan));
            }

            SharedContext.App.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.AcceptTerms>()
                    .That.AsCheckBox().LetsClick()
                .AndAlso().Should().HaveElement<FormsPage.Registration.Submit>()
                    .That.AsButton().LetsClick();
        }

        public static void LetsCheckTheResult(string fullName, string email, string country, string plan)
        {
            SharedContext.App.Should().HaveSection<FormsPage.Result>()
                .That.Should().HaveElement<FormsPage.Result.Title>()
                    .That.AsLabelText().Should().HaveValue("Registration received")
                .AndAlso().Should().HaveElement<FormsPage.Result.FullName>()
                    .That.AsLabelText().Should().HaveValue(fullName)
                .AndAlso().Should().HaveElement<FormsPage.Result.Email>()
                    .That.AsLabelText().Should().HaveValue(email)
                .AndAlso().Should().HaveElement<FormsPage.Result.Country>()
                    .That.AsLabelText().Should().HaveValue(country)
                .AndAlso().Should().HaveElement<FormsPage.Result.Plan>()
                    .That.AsLabelText().Should().HaveValue(plan);
        }

        // A step can also give something back, for the test to check
        public static string LetsReadTheEmailError()
        {
            return SharedContext.App.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<FormsPage.Registration.EmailError>()
                    .That.AsLabelText().LetsGetValue();
        }

        private static void LetsChooseThePlan<TPlan>() where TPlan : IElementContext, new()
        {
            SharedContext.App.Should().HaveSection<FormsPage.Registration>()
                .That.Should().HaveElement<TPlan>()
                    .That.AsRadioButton().LetsClick();
        }
    }
}
  • The names start with Lets…, like VAF's own actions, so a test reads as one story: LetsOpenTheForm, LetsRegister, LetsCheckTheResult.
  • A step takes what the test decides — the name, the plan — and hides how it is done. The test says "Team"; which radio button that is, only the step knows.
  • A step can return a value, like LetsReadTheEmailError, when the test should decide what to expect.
4

Tests made of steps

Replace RegistrationTests.cs with a version that uses the steps:

FirstVafTest/Tests/RegistrationTests.cs On GitHub
using FirstVafTest.Common;
using FirstVafTest.SharedSteps;
using NUnit.Framework;

namespace FirstVafTest.Tests
{
    // The test of lesson 2, written with shared steps: it reads as what the user does
    public class RegistrationTests : TestBase
    {
        // TestBase has started the browser; this opens the form and fills SharedContext.App
        [SetUp]
        public void Open() => FormsSteps.LetsOpenTheForm();

        [TestCase("Free")]
        [TestCase("Team")]
        [TestCase("Enterprise")]
        public void ValidRegistrationIsShownBack(string plan)
        {
            FormsSteps.LetsRegister("Ada Lovelace", "ada@example.com", "analytical", "Croatia", plan);
            FormsSteps.LetsCheckTheResult("Ada Lovelace", "ada@example.com", "Croatia", plan);
        }

        [TestCase("ada.example.com", "This does not look like an email address.")]
        [TestCase("", "Please enter your email address.")]
        public void WrongEmailIsReported(string email, string error)
        {
            FormsSteps.LetsRegister("Ada Lovelace", email, "analytical", "Croatia");

            Assert.That(FormsSteps.LetsReadTheEmailError(), Is.EqualTo(error));
        }
    }
}

Compare it with the test of lesson 2: no locators, no chains, no browser. Because a test is now only data and steps, more cases cost one line each — [TestCase] runs the same test with other values: every plan, and two wrong emails.

5

Move the other tests onto the test base

The other test classes change the same way. The first test, for example:

FirstVafTest/Tests/FirstTests.cs On GitHub
using FirstVafTest.Common;
using NUnit.Framework;
using VisionAutomationFramework.Extensions;

namespace FirstVafTest.Tests
{
    public class FirstTests : TestBase
    {
        [Test]
        public void OpensThePracticeForm()
        {
            SharedContext.LetsOpen("/practice/forms");

            SharedContext.Browser.Should().HaveTitle("Practice: forms — Vision Automation Framework");
        }
    }
}

In each class:

  1. derive from TestBase;
  2. remove the browser: the fields, [SetUp] / [TearDown] that start and close it, and try / finally;
  3. open the page with SharedContext.LetsOpen("/practice/…") or a step such as FormsSteps.LetsOpenTheForm();
  4. start every chain from SharedContext.App.

The result, for comparison: FormControlsTests.cs, LocatorTests.cs, DateAndUploadTests.cs.

These tests keep their chains, and that is fine. They test single controls, so the chain is the test. Steps pay off for flows that several tests repeat — registering, signing in, placing an order.

6

Run it

Run all tests: seventeen pass. The project now has four kinds of files, each with one job:

FirstVafTest
├── Common          the browser and the page context
│   ├── SharedContext.cs
│   └── TestBase.cs
├── PageObjects     where things are on the page
├── SharedSteps     what the user does
│   └── FormsSteps.cs
├── Tests           what must be true
└── FirstVafTest.csproj

When the page changes, you change a page object. When a flow changes, a step. When a requirement changes, a test. Each change happens in one place.

Compare with the reference code

The project as it is at the end of this lesson is tag lesson-5 of VAF-Tutorial. If your test does not pass, compare your files with it, or get it with git checkout lesson-5.