To support .NET Developers and Testers in building Web application tests using composable PageObjects.
-
Removing direct dependencies on specific test tools in PageModels and Tests and depending on this library to abstract common components and common query providers.
-
Allows sharing components among different types of tests (Presentation layer tests, Integration Tests, EndToEnd UI tests) by substituting querying providers.
IPage
- Role: mark pages with thisIPageFactory
- Role: create pages with thisIComponentFactory<IComponent>
Role: create components with this
The Dfe.Testing.Pages library supports the below providers
AngleSharp
services.AddAngleSharp<TApplicationProgram>(); // TApplicationProgram is your .NET Program class for your Web Application
Selenium.WebDriver
services.AddWebDriver();
In order to use the library you will need to setup [DependencyInjection](TODO LINK) inside of your tests and register the provider you want for that test suite. Below is an example of how you setup DependencyInjection.
// This uses the Singleton pattern that wraps the DependencyInjection container allowing for the services to be configured and built once.
// The `IServiceProvider` once built, is delegated responsibility for creating registered implementations of types and managing their lifetimes.
// An `IServiceScope` is a child scope of the root DependencyInjection container, when you resolve through a scope, after you dispose of the scope - `Scoped` dependencies are disposed of.
.AddSingleton<TImplementation>()
.AddScoped<TInterface, TImplementation>();
_scope.Resolve<TInterface>();
_scope.Dispose(); // My scoped instance gets disposed, my Singleton remains
internal sealed class DependencyInjection
{
private static readonly DependencyInjection _instance = new();
private readonly IServiceProvider _serviceProvider;
static DependencyInjection()
{
}
private DependencyInjection()
{
IServiceCollection services = new ServiceCollection()
.AddPages()
// ToAddAngleSharp .AddAngleSharp<Program>();
// ToAddWebDriver .AddWebDriver();
_serviceProvider = services.BuildServiceProvider();
}
public static DependencyInjection Instance
{
get
{
return _instance;
}
}
internal IServiceScope CreateScope() => _serviceProvider.CreateScope();
}
// Separately you want to consume this in your BaseTestClass
public abstract class BaseTest : IDisposable
{
private readonly IServiceScope _serviceScope;
protected BaseHttpTest()
{
_serviceScope = DependencyInjection.Instance.CreateScope();
}
protected T GetTestService<T>()
=> _serviceScope.ServiceProvider.GetService<T>()
?? throw new ArgumentNullException($"Unable to resolve type {typeof(T)}");
public void Dispose()
{
GC.SuppressFinalize(this);
_serviceScope.Dispose();
}
}
// Any test class you inherit from BaseTest gets access to the container and a new scope is created per test
public sealed class MyTestClass : BaseTest
{
[Fact]
public async Task MyTest()
{
IPageFactory pageFactory = GetTestService<IPageFactory>(); // is available
}
}
When building PageModels you want to:
public sealed class MyPage : IPage
services....
services.AddTransient<IPage, HomePage>();
// NOTE make sure your register the components!
services.AddTransient<SearchComponent>();
services.AddTransient<FilterComponent>();
public sealed class HomePage
{
public HomePage(
NavigationBarComponent navBar,
SearchComponent search,
FilterComponent filter)
{
Search = search ?? throw new ArgumentNullException(nameof(search));
Filter = filter ?? throw new ArgumentNullException(nameof(filter));
NavBar = navBar ?? throw new ArgumentNullException(nameof(navBar));
}
// Can reuse these across PageModels
public NavigationBarComponent NavBar { get; }
public SearchComponent Search { get; }
public FilterComponent Filter { get; }
}
Create ALL of your PageModels using the IPageFactory
public sealed class MyTestClass : BaseTest{
[Fact]
public async Task MyTest()
{
HttpRequestMessage homePageRequest = new()
{
Uri = new("/")
}
HomePage homePage = await GetTestService<IPageFactory>().CreatePageAsync<HomePage>(homePageRequest);
}
}
// Basic types example
homePage.GetHeading().Should().Be("Heading");
public sealed class HomePage
{
public string GetHeading() => _headingFactory.GetHeadings().Where(t => t.Type == H1).Text;
}
// GDSComponent provided by the library
// Record to give value-object semantics
public record GDSTextInput
{
public required string Name { get; init; }
public required string Value { get; init; }
public required string? PlaceHolder { get; init; } = null;
public required string? Type { get; init; } = null;
}
GDSTextInput textInput = new()
{
Name = "searchKeyWord",
Value = "",
PlaceHolder = "Search by keyword",
Type = "text"
};
// Test
homePage.TextInput.Should().Be(textInput);
// Page
public sealed class HomePage
{
public GDSTextInput GetSearchInput() => _factoryForInputs_.Create();
}
// CUSTOM COMPLEX APPLICATION TYPE
public record Facet(string Name, IEnumerable<FacetValue> FacetValues);
public record FacetValue(string Label, string Value);
homePage.GetDisplayedFacets().Should().Be(new[]
{
new Facet
(
Name: "Facet name",
FacetValues: []
),
new Facet
(
Name: "Facet name",
FacetValues: []
)
})
public sealed class HomePage
{
public IEnumerable<Facet> GetDisplayedFacets()
=> _formFactory.Get().FieldSets
.Select(
(fieldSet) => new Facet(
Name: fieldSet.Legend,
FacetValues: fieldSet.Checkboxes.Select(
(checkbox) => new FacetValue(checkbox.Label, checkbox.Value))));
}