From cb39abca5a6c7dfaabc66e9f6f9c74aa7e861185 Mon Sep 17 00:00:00 2001
From: Pavel Vostretsov
Date: Thu, 15 Feb 2024 14:31:31 +0500
Subject: [PATCH] Update README.md
---
README.md | 228 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 227 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0bb876c..1324b3c 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,227 @@
-# nunit-middlewares
\ No newline at end of file
+# nunit-middlewares
+
+Use middleware pattern to write tests in concise and comprehensive manner. And ditch test bases.
+
+| | Build Status |
+|-------------------------------------|:--------------: |
+| NUnit.Middlewares | [![NuGet Status](https://img.shields.io/nuget/v/SkbKontur.NUnit.Middlewares.svg)](https://www.nuget.org/packages/SkbKontur.NUnit.Middlewares/) |
+| Build | [![Build status](https://github.com/skbkontur/nunit-middlewares/actions/workflows/actions.yml/badge.svg)](https://github.com/skbkontur/nunit-middlewares/actions) |
+
+## Test setup middlewares
+
+Inspired by ASP.NET Core [middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware), the main idea of test middlewares can be summarized by this image:
+
+![nunit-middlewares](https://github.com/skbkontur/nunit-middlewares/assets/5417867/9707428f-11ec-4353-ac96-7fdf70200a47)
+
+Here we focus on *behaviours* that we want to add to our test rather than focusing on implementing test lifecycle methods provided by NUnit.
+
+`suite`, `fixture` and `test` in the image above are just `ISetupBuilder` that can accept either raw setup functions or anything that implements simple `ISetup` interface:
+
+![setup-builder](https://github.com/skbkontur/nunit-middlewares/assets/5417867/e4adb7c6-2078-401e-9bac-539f89ffec54)
+
+## Simple test base
+
+To inject this new behaviour into our tests, we will use two simple base classes: `SimpleSuiteBase` and `SimpleTestBase`, our tests from first image can be set up as follows:
+
+```csharp
+[SetUpFixture]
+public class PlaywrightSuite : SimpleSuiteBase
+{
+ protected override void Configure(ISetupBuilder suite)
+ {
+ suite
+ .UseHostingEnvironment()
+ .UseSimpleContainer()
+ .UseSetup();
+ }
+}
+
+public class BusinessObjectsSearchTests : SimpleTestBase
+{
+ [Injected] // Injected from container by `InitializeInjectedSetup`
+ private readonly IUserProvider userProvider;
+
+ protected override void Configure(ISetupBuilder fixture, ISetupBuilder test)
+ {
+ fixture
+ .UseSetup();
+
+ test
+ .UseSetup();
+ }
+
+ [Test]
+ public async Task BasicTest()
+ {
+ // every test gets its own browser, thus making tests easily parallelizable
+ var browser = SimpleTestContext.Current.Get();
+
+ await browser.LoginAsync(userProvider.DefaultUser);
+ await browser.Page.GotoAsync("https://google.com");
+ await browser.Page.GetByTitle("Search").FillAsync("nunit");
+ }
+}
+```
+
+## Composition over inheritance
+
+With the power of C#'s extension methods, we can use composition of setups instead of relying on inheritance. For example, here's how setup for our container can be written:
+
+```csharp
+public static class SetupExtensions
+{
+ public static ISetupBuilder UseSimpleContainer(
+ this ISetupBuilder builder,
+ Action? configure = null)
+ {
+ return builder
+ // our container needs hosting environment, hence we should always set it up,
+ // but if it was already set up earlier, we will use existing environment
+ .UseSetup(new HostingEnvironmentSetup(setupOnlyIfNotExists: true))
+ .UseSetup(new SimpleContainerSetup(configure));
+ }
+}
+
+public class SimpleContainerSetup : ISetup
+{
+ private readonly Action? configure;
+
+ public SimpleContainerSetup(Action? configure)
+ {
+ this.configure = configure;
+ }
+
+ public Task SetUpAsync(ITest test)
+ {
+ var environment = test.GetFromThisOrParentContext();
+ var container = ContainerFactory.NewContainer(environment, configure);
+ test.Properties.Set(container); // save container to current test context
+
+ return Task.CompletedTask;
+ }
+
+ public Task TearDownAsync(ITest test)
+ {
+ var container = test.Properties.Get();
+ container.Dispose();
+
+ return Task.CompletedTask;
+ }
+}
+```
+
+Using these building blocks, we can move all the complexity of setups to separate, smaller code pieces (`ISetup`s), and make setups more reusable in the process.
+
+## Simple test context
+
+In our `BasicTest` above we used `SimpleTestContext.Current.Get()` to get browser that we set up in `BrowserPerTestSetup`. Also, in `SimpleContainerSetup` we used `GetFromThisOrParentContext` method that can access items that previous setups have set up. How does it work? Good news is that we can use built-in NUnit features to build such test context.
+
+`TestExecutionContext.CurrentContext.CurrentTest` - current test, implements `ITest`
+
+How do we get container/browser from suite context in our test? Every test has property `IPropertyBag Properties`.
+
+Tests in NUnit are represented by a tree-like structure, and `ITest` has access to parent through `ITest Parent` property. Parent for test method is test fixture, parent for fixture is suite and so on.
+
+That means we can search for *context item* of interest in parent, if not found - in parent's parent
+
+To ensure everything is working as intended, parent's *context item*s should be used as **readonly**
+
+In our example from first image, test context will look something like this:
+
+![test-context](https://github.com/skbkontur/nunit-middlewares/assets/5417867/c70b41d6-5f3f-485a-9e9d-7616b3797232)
+
+Both `SimpleTestContext` and `GetFromThisOrParentContext` are just `ITest` wrappers that search for context value in `ITest`'s `Properties` recursively
+
+## Why are test bases a problem?
+
+To make a point, let's try to rewrite test above without our testing machinery.
+
+Let's start with `BusinessObjectsSearchTests.cs`:
+
+```csharp
+public class BusinessObjectsSearchTests : PlaywrightTestBase
+{
+ [Injected]
+ private readonly IUserProvider userProvider;
+
+ [Test]
+ public async Task BasicTest()
+ {
+ // every test gets its own browser, thus making tests easily parallelizable
+ await using var browser = await BrowserPerTest();
+
+ await browser.LoginAsync(userProvider.DefaultUser);
+ await browser.Page.GotoAsync("https://google.com");
+ await browser.Page.GetByTitle("Search").FillAsync("nunit");
+ }
+}
+```
+
+So far so good, notice that we moved `BrowserPerTestSetup` into the test itself. A neat trick that would be more difficult if we had more per test instances to set up.
+
+`PlaywrightTestBase` looks simple enough. But we had to make our Browser `IAsyncDisposable`:
+
+```csharp
+public class PlaywrightTestBase : SimpleContainerTestBase
+{
+ protected IPlaywright playwright;
+ protected IBrowser browser;
+
+ [OneTimeSetUp]
+ public async Task SetUpPlaywright()
+ {
+ playwright = await Playwright.CreateAsync();
+ browser = await playwright.Chromium.LaunchAsync()
+ }
+
+ [OneTimeTearDown]
+ public async Task TearDownPlaywright()
+ {
+ await browser.DisposeAsync().ConfigureAwait(false);
+ playwright.Dispose();
+ }
+
+ protected async Task BrowserPerTest()
+ {
+ var page = await browser.NewPageAsync();
+ return new Browser(page); // now Browser is responsible for disposing of page
+ }
+}
+```
+
+How deep does this rabbit hole go? Let's dive into `SimpleContainerTestBase`:
+
+```csharp
+public class SimpleContainerTestBase
+{
+ protected IContainer container;
+
+ [OneTimeSetUp]
+ public void SetUpContainer()
+ {
+ var environment = HostingEnvironment.Create();
+ container = ContainerFactory.NewContainer(environment, ConfigureContainer);
+ ContainerFactory.InitializeInjectedFields(container, this);
+ }
+
+ [OneTimeTearDown]
+ public void TearDownContainer()
+ {
+ container.Dispose();
+ }
+
+ protected virtual void ConfigureContainer(ContainerBuilder builder)
+ {
+ }
+}
+```
+
+Now it doesn't look that bad. What did we miss? Quite a few things:
+- it was harder to setup items per test and keep tests parallelizable
+- to shorten chain of inheritance, we tightly integrated setup of HostingEnvironment and Container and forgot to dispose of hosting environment
+- we set up container and hosting environment for each test, before we only set it up once. Refactoring it can be a PITA, especially if `container` or `browser` field is referenced in our tests. On the other hand, when using nunit-middlewares, we can refactor such case by moving two lines of code.
+- what if many of our test fixtures need an organization to work with? would we make `class OrganizationTestBase : PlaywrightTestBase`? and if we need an organization, but don't need browser?
+- our example is rather simple, in more complex cases, our test bases can quickly become a nightmare to debug and extend
+
+Excellent example of a complex case is playwright integration with nunit in official [Playwright.NUnit](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.NUnit) package:
+- it has `PageTest` that inherits `ContextTest` that inherits `BrowserTest` that inherits `PlaywrightTest` that inherits `WorkerAwareTest`... whoa
\ No newline at end of file