To support building .NET tests for web applications with composable PageObjects. This library targets usage of GDS components in those applications.
Goals include;
-
Provide testers with access to GDS components so they can be used as building blocks for PageObjects.
-
Ensure different types of tests for a web application can use the same PageObjects.
-
Ensure separation of concerns is followed closer between Test, PageObject, Document
-
Ensure your test project is using dependency injection - DI
-
Choose a query library for your test project
-
Register your pages and components in your tests DI using
IPageObject
-
Use library provided Components in your pages with
ComponentFactory<TComponent>
-
Access pages in your tests with
IPageObjectFactory
-
For further help see docs or reach out on dfe-slack
public-pageobjects-testing-library
// to use default AngleSharpOptions
services.AddAngleSharp();
services.AddWebApplicationFactory<TApplicationProgram>(); // TApplicationProgram is your Program class from your .NET Web Application
// to configure WebDriverOptions
services.AddAngleSharp(t => {
});
// to use default WebDriverOptions
services.AddWebDriver();
// to configure WebDriverOptions
services.AddWebDriver(t => {
t.Browser.BrowserName = "edge"; // browser being used
t.Browser.BrowserMajorVersion = "131"; // version of browser
t.Browser.ShowBrowser = false; // should the browser display a screen or be headless
t.Browser.PageLoadTimeoutSeconds = 30; // how long should WebDriver wait for the page to load
t.Browser.ViewportHeight = 1080;
t.Browser.ViewportWidth = 1920; // the dimensions of the browser
t.Browser.EnableIncognito = false; // should the browser start as in incognito mode
t.Browser.EnableAuthenticationBypass = false; // enable the network interception module
});
// TODO suggest to tester managing configuration through Binding? Provide a default JSON?
IComponentFactory<TComponent>
Role: create components with this
// example of using library
public sealed class MyPage
{
IComponentFactory<GDSHeaderComponent> _headerFactory
// constructor
public MyPage(IComponentFactory<GDSHeaderComponent> headerFactory)
{
_headerFactory = headerFactory;
}
// simplest usage of creating a GDSHeaderComponent
public GDSHeaderComponent => _headerFactory.Create(... options).Created
}
testServices.AddTransient<HomePage>(); // page
testServices.AddTransient<NavigationBarComponent>(); // application component
public sealed class HomePage
{
public HomePage(
NavigationBarComponent navBar,
SearchComponent search)
{
NavBar = navBar ?? throw new ArgumentNullException(nameof(navBar));
Search = search ?? throw new ArgumentNullException(nameof(search));
}
// Reuse these across Pages
public NavigationBarComponent NavBar { get; }
public SearchComponent Search { get; }
}
Note Ensure you have Registered your pages and application components
public sealed class MyTestClass : BaseTest
{
[Fact]
public async Task MyTest()
{
// create document that the page will use
IDocumentService documentService = GetTestService<IDocumentService>();
// pageobjects use an document to fulfil their query requests
await documentService.RequestDocumentAsync(
(t) => t.SetPath("/"));
// Resolve page through DI
HomePage page = GetTestService<HomePage>();
}
}
// This uses the Singleton pattern that wraps the DependencyInjection container ensuring a single instance of the container and, for services to be registered via `IServiceCollection`
internal sealed class DependencyInjection
{
private static readonly DependencyInjection _instance = new();
private readonly IServiceProvider _serviceProvider;
static DependencyInjection()
{
}
private DependencyInjection()
{
IServiceCollection services = new ServiceCollection()
// ToAddAngleSharp .AddAngleSharp<Program>();
// ToAddWebDriver .AddWebDriver();
//
services.AddTransient<IPageObject, ApplicationHomePage>();
services.AddTransient<ApplicationComponent>();
_serviceProvider = services.BuildServiceProvider();
}
public static DependencyInjection Instance
{
get
{
return _instance;
}
}
internal IServiceScope CreateScope() => _serviceProvider.CreateScope();
}
// Separately you may want to make this Scope started and disposed in a base test class.
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.
// NOTE by inheriting - this does making overriding your container configuration not possible as the BaseTest is created (and it's ServiceProvider built)
// before you can override parts of it in your test.
public sealed class MyTestClass : BaseTest
{
[Fact]
public async Task MyTest()
{
IDocumentSession documentSession = GetTestService<IDocumentSession>(); // is available
}
}