Lesson 5: Shared steps and a test base
Move the chains out of the tests into steps named after what the user does, and the browser into a test base.
Tutorials · Lesson 5 of 6 · about 30 minutes
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.
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.
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.
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.
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.
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:
- derive from
TestBase; - remove the browser: the fields,
[SetUp]/[TearDown]that start and close it, andtry/finally; - open the page with
SharedContext.LetsOpen("/practice/…")or a step such asFormsSteps.LetsOpenTheForm(); - 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.
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.