Guide
How a VAF suite is built, in the order you build it: map the UI as page objects, turn user actions into shared steps, write tests that read like scenarios.
1. Page objects
Map the application once, as small classes with one locator each, in three levels:
-
The app — one
IPageContextclass for the whole application, the root everything else is found in. -
Sections — one
ISectionContextclass per screen or component: a header, a sign-in form, a grid. -
Elements — one
IElementContextclass per field, button or text, nested inside its section, soLoginForm.Emailsays exactly where the field lives.
public class AppPage : IPageContext
{
public By PageFindMechanism { get; set; } = By.TagName("body");
}
public class LoginForm : ISectionContext
{
public By SectionFindMechanism { get; set; } = By.Id("login-form");
public class Email : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("email");
}
public class Password : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("password");
}
public class Submit : IElementContext
{
public By ElementFindMechanism { get; set; } = By.CssSelector("button[type='submit']");
}
public class Error : IElementContext
{
public By ElementFindMechanism { get; set; } = By.CssSelector(".alert-danger");
}
}
public class Header : ISectionContext
{
public By SectionFindMechanism { get; set; } = By.CssSelector("header");
public class SignIn : IElementContext
{
public By ElementFindMechanism { get; set; } = By.LinkText("Sign in");
}
public class UserName : IElementContext
{
public By ElementFindMechanism { get; set; } = By.Id("user-name");
}
}
Prefer ids and CSS selectors to absolute XPath such as /html/body/div[7]: they survive
layout changes, and when a locator does break there is exactly one place to fix it.
2. Control types
After HaveElement<T>(), say what kind of control the element is. That decides the
actions (Lets…) and checks (after Should()) you get. The most used ones:
| Call | Control | Actions | Checks |
|---|---|---|---|
AsButton(), AsMenu() | Button, menu item | LetsClick, LetsGetValue | HaveValue, ContainValue, BeEnabled, BeDisabled |
AsLink() | Link | LetsClick, LetsGetHrefValue | HaveValue, NotHaveValue |
AsTextInputField() | Single-line input (text, email, password…) | LetsInsert, LetsClear, LetsSendKey | HaveValue, Contain, BeTypeOf |
AsTextAreaField() | Multi-line text area | LetsInsert, LetsClear | HaveValue |
AsSelectBox() | Drop-down list (<select>) | LetsSelect, LetsSelectRandom, LetsGetOptions | BeSelected, HaveOption, HaveOptions |
AsCheckBox() | Check box | LetsClick | BeChecked, BeUnChecked |
AsRadioButton() | Radio button | LetsClick | BeSelected, BeUnSelected |
AsLabelText() | Any text: label, heading, message | LetsGetValue | HaveValue, Contain, HaveOneOfValue, BeBolded |
AsToolTipText() | Tooltip | LetsGetValue | HaveValue, BeBolded |
AsCounterField() | Number shown on the page (counter, badge) | LetsGetValue | HaveValue |
AsTableBody() | Body of a table (<tbody>) | LetsGetNumberOfRows | HaveNumberOfRows |
AsUploadFileField() | File input | LetsUpload | BeDisabled, BeFocused |
AsSlideBar() | Slider | LetsSetValue, LetsMoveToPercentage | |
AsDateTimeFlatpickerV4(), AsDateTimeBootstrap3DatepickerV4() | Date picker (flatpickr 4, Bootstrap 3 datetimepicker 4) | LetsSelectDate | HaveValue |
Most control types also have LetsTab and the checks BeEnabled,
BeDisabled and BeFocused.
3. Shared steps
This is where VAF pays off. Wrap each fluent chain in a static shared step named
after what it does for the user — LetsSignIn, LetsCheckSignedInAs — and
let every test use it. The page context from DisplayPage<AppPage>().That is kept
once and every step starts from it.
public static class SharedSteps
{
public static BrowserActions Browser { get; private set; }
public static PageActions<AppPage> App { get; private set; }
public static void LetsOpenTheShop(string url)
{
Browser = Testing.On<Chrome>().LetsUseLoader<Spinner>();
App = Browser.LetsNavigateTo(url)
.Should().DisplayPage<AppPage>().That;
}
public static void LetsSignIn(string email, string password)
{
App.Should().HaveSection<Header>()
.That.Should().HaveElement<Header.SignIn>()
.That.AsLink().LetsClick();
App.Should().HaveSection<LoginForm>()
.That.Should().HaveElement<LoginForm.Email>()
.That.AsTextInputField().LetsInsert(email)
.AndAlso().Should().HaveElement<LoginForm.Password>()
.That.AsTextInputField().LetsInsert(password)
.AndAlso().Should().HaveElement<LoginForm.Submit>()
.That.AsButton().LetsClick();
}
public static void LetsCheckSignedInAs(string name)
{
App.Should().HaveSection<Header>()
.That.Should().HaveElement<Header.UserName>()
.That.AsLabelText().Should().HaveValue(name);
}
public static string LetsReadLoginError()
{
return App.Should().HaveSection<LoginForm>()
.That.Should().HaveElement<LoginForm.Error>()
.That.AsLabelText().LetsGetValue();
}
public static void LetsCloseTheBrowser() => Browser.Scope.Driver.Quit();
}
When the sign-in form changes, one shared step changes. Every test that signs in keeps working, and none of them had to know how signing in is done.
4. Tests
Tests call shared steps and contain no locators and no fluent chains of their own. They are short, read as scenarios, and data-driven tests come for free from the test framework.
[TestFixture]
public class SignInTests
{
[SetUp]
public void OpenShop() => SharedSteps.LetsOpenTheShop("https://shop.example.com");
[TearDown]
public void Report()
{
Testing.ConvertLogToHtml(TestContext.CurrentContext.WorkDirectory,
TestContext.CurrentContext.Test.Name);
SharedSteps.LetsCloseTheBrowser();
}
[Test]
public void SignsInWithValidCredentials()
{
SharedSteps.LetsSignIn("qa@example.com", "not-a-real-password");
SharedSteps.LetsCheckSignedInAs("QA Engineer");
}
[TestCase("qa@example.com", "wrong-password")]
[TestCase("unknown@example.com", "not-a-real-password")]
public void RejectsInvalidCredentials(string email, string password)
{
SharedSteps.LetsSignIn(email, password);
Assert.That(SharedSteps.LetsReadLoginError(), Does.Contain("Invalid"));
}
}
Using MSTest? VAF works with it as well. Add this to the test class:
[TestInitialize]
public void Initialize()
{
// VAF asserts with NUnit internally. Under MSTest this clears NUnit's result
// state, so a failure in one test does not carry over into the next one.
new NUnit.Framework.Internal.TestExecutionContext.IsolatedContext();
}
5. Checking and reading values
Assert a value with HaveValue, read one with LetsGetValue when the test
needs it later, and use BePresent for content that is only sometimes there — it
answers true or false instead of failing.
// Assert a value: fails the test with a clear message when it differs
App.Should().HaveSection<Header>()
.That.Should().HaveElement<Header.UserName>()
.That.AsLabelText().Should().HaveValue("QA Engineer");
// Read a value to use it later in the test
string error = App.Should().HaveSection<LoginForm>()
.That.Should().HaveElement<LoginForm.Error>()
.That.AsLabelText().LetsGetValue();
// Branch on content that is only sometimes there — returns true or false, never fails
bool shown = App.Should().HaveSection<LoginForm>()
.That.Should().BePresent<LoginForm.Error>(seconds: 3);
6. Waiting
Every assertion waits for its page, section or element before checking it, so tests contain no
sleeps and no WebDriverWait. If the application shows a loading indicator, tell VAF
once and every step waits for it to disappear:
public class Spinner : IElementContext
{
public By ElementFindMechanism { get; set; } = By.CssSelector(".spinner");
}
// Every following step waits until the spinner is gone
Testing.On<Chrome>()
.LetsUseLoader<Spinner>()
.LetsNavigateTo("https://shop.example.com");
After an action that starts a longer load, add WaitForLoad():
App.Should().HaveSection<LoginForm>()
.That.Should().HaveElement<LoginForm.Submit>()
.That.AsButton().LetsClick()
.WaitForLoad();
Timeouts come from app.config of the test project, in seconds:
<appSettings>
<add key="ElementWaitTimeout" value="10" />
<add key="LoaderWaitTimeout" value="60" />
</appSettings>
7. Log and HTML report
Every action and assertion is written to the test log as it happens. Turn it into an HTML page at the end of each test — when something fails, the report shows the last step that worked.
[TearDown]
public void Report()
{
// Every action and assertion of the test, as an HTML page next to the results
Testing.ConvertLogToHtml(TestContext.CurrentContext.WorkDirectory,
TestContext.CurrentContext.Test.Name);
}
8. Reading a chain
Testing.On<Chrome>() |
Starts the browser and the test log. Firefox and Safari work the same way. |
.Lets…() |
Actions: LetsNavigateTo, LetsClick, LetsInsert, LetsSelect, LetsUpload… |
.Should() |
Starts an assertion, which waits for its target before checking it. |
.That |
Moves into what was just asserted, so the next step works inside that page, section or element. |
.As…() |
Says what kind of control the element is. |
.AndAlso() |
Steps back to the enclosing section, so the chain can continue with its next element. |