# bUnit - Blazor Component Testing **bUnit** ist eine Testing Library für Blazor Komponenten. Mit bUnit kann man: - Komponenten in Tests mit C# oder Razor Syntax einrichten und definieren - Ergebnisse mit semantischen HTML Vergleichen überprüfen - Mit Komponenten interagieren, sie untersuchen und Event Handlers ausführen - Parameter, Cascading Values und Services in Komponenten injezieren - *IJSRuntime*, Blazor Authentication und Authorization sowie weiteres mocken bUnit unterstützt sowohl C# wie auch Razor als Syntax. Ich werde mich für die Beispiele auf C# beschränken. ## Einfache Tests Schreiben Für unseren ersten Test wollen wir eine `<Foo>` Komponente Testen: ```XML <h1>Hello World</h1> ``` Für diese Anleitung werden wir als grundlegende Testing Library *NUnit* verwenden, wobei auch *xUnit* oder *MSTest* verwendet werden könnte: ```csharp using NUnit.Framework; using Bunit; public class FooTest : BunitTestContex { [Test] public void Foo_ShouldRenderCorrectMarkup() { // Act var cut = RenderComponent<Foo>(); // Assert cut.MarkupMatches("<h1>Hello World</h1>"); } } ``` Weil NUnit nur eine Test Klasse für alle Tests initialisiert, können wir nicht direkt von *TestContext* erben, da wir eine frische *TestContext* Instanz für jeden Test wollen. Stattdessen erstellen wir die Helfer-Klasse *BunitTestContext*: ```csharp using Bunit; using NUnit.Framework; public abstract class BunitTestContext : TestContextWrapper { [SetUp] public void Setup() => TestContext = new Bunit.TestContext(); [TearDown] public void TearDown() => TestContext?.Dispose(); } ``` Unser Test macht folgendes: 1. Er rendert die `<Foo>` Komponente mit hilfe des TestContext 2. Er überprüft, ob das gerenderte Markup mit dem erwarteten übereinstimmt > [!tip] > In bUnit Tests wird gerne die Abkürzung **cut** für _**C**omponent **U**nder **T**esting_ genutzt. ## Parameter an Komponenten übergeben Es gibt zwei Methoden, welche das Übergeben von Parametern erlauben: - Die `RenderComponent` Methode, welche die Komponente initial rendert - Die `SetParametersAndRender` Methode, welche es ermöglicht einer bereits gerenderten Komponente neue Parameter zu übergeben ### Nicht-Blazor Type Parameter Angenommen wir haben folgende Parameter in unserer `<FooComponent>`: ```csharp <h1>@Name</h1> @code { [Parameter] public string Name { get; set; } [Parameter] public List<decimal> Grades { get; set; } } ``` Um diese Parameter in unserem bUnit Test zu setzen, können wir folgenden Code nutzen: ```csharp public class FooTest : TestContextWrapper { [Test] public void FooComponent_ShouldRenderCorrectMarkup() { // Arrange var name = "Bar"; var grades = new List<decimal>() { 5.5m, 4.8m, 5.2m }; // Act var cut = RenderComponent<FooComponent>(parameters => parameters .Add(p => p.Name, name) .Add(p => p.Grades, grades) ); // Assert cut.MarkupMatches("<h1>" + name + "</h1>"); } } ``` ### EventCallback Parameter Folgendermassen können wir auch einen *EventCallback* Parameter setzen: ```csharp <h1>@Name</h1> @code { [Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; } [Parameter] public EventCallback OnSomething { get; set; } } ``` ```csharp public class FooTest : TestContextWrapper { [Test] public void FooComponent_SetEventCallbackParameter() { // Arrange Action<MouseEventArgs> onClickHandler = _ => { }; Action onSomethingHandler = () => { }; // Act var cut = RenderComponent<FooComponent>(parameters => parameters .Add(p => p.OnClick, onClickHandler) .Add(p => p.OnSomething, onSomethingHandler) ); // Assert // ... } } ``` ### Kaskadierende Parameter und Werte Kaskadierende Parameter werden über das `[CascadingParameter]` Attribut ausgezeichnet. Es gibt benannte und unbenannte Cascading Parameter. Unsere Beispiele werden wir an folgender Komponente machen: ```csharp @code { [CascadingParameter] public bool IsDarkTheme { get; set; } [CascadingParameter(Name = "AuthenticatedUser")] public string UserName { get; set; } } ``` #### Unbenannte kaskadierende Parameter übergeben Zuerst wollen wir den unbenannten Parameter `IsDarkTheme` übergeben: ```csharp public class FooTest : TestContextWrapper { [Test] public void FooComponent_SetUnnamedCascadingParameter() { // Arrange var isDarkTheme = false; // Act var cut = RenderComponent<FooComponent>(parameters => parameters .Add(p => p.IsDarkTheme, isDarkTheme) ); // Assert // ... } } ``` Auch hier verwenden wir wieder die `Add` Methode des CollectionBuilders. Wir übergeben den Wert explizit dem Parameter. #### Benannte kaskadierende Parameter übergeben ```csharp public class FooTest : TestContextWrapper { [Test] public void FooComponent_SetNamedCascadingParameter() { // Arrange var userName = false; // Act var cut = RenderComponent<FooComponent>(parameters => parameters .Add(p => p.UserName, userName) ); // Assert // ... } } ``` Hier wird der Name des Felds und **nicht** der Name des Parameters verwendet (`UserName` anstelle von `AuthenticatedUser`). ### Bidirektionale Anbindung von Parametern Es ist auch möglich, Komponenten Parameter bidirektional zu binden, wie zum Beispiel folgendes `Value` und `ValueChanged` Parameter Paar: ```csharp @code { [Parameter] public string Value { get; set; } = string.Empty; [Parameter] public EventCallback<string> ValueChanged { get; set; } } ``` ```csharp public class FooTest : TestContextWrapper { [Test] public void FooComponent_BindTwoWayParameter() { // Arrange var currentValue = string.Empty; // Act var cut = RenderComponent<FooComponent>(parameters => parameters .Bind( p => p.Value, currentValue, newValue => currentValue = newValue ) ); // Assert // ... } } ``` Dieses Beispiel nutzt die `Bind` Methode um eine bidirektionale Verbindung zwischen den `Value` und `ValueChanged` Parametern mit der lokalen Variable `currentValue` zu erstellen. Die `Bind` Methode ist eine kürzere Art, den `Value` und `ValueChanged` Parameter mit `Add` individuell zu setzen. ## Mit Komponente interagieren Blazor ermöglicht es, viele Event Handlers an Elemente zu binden (z.B.: `@onclick="OnClick"`). bUnit kommt mit *Event Dispatch* Helfermethoden, welche es vereinfacht diese Event Handlers auszuführen. Die Eingebauten *Event Dispatch* Helfer sind: - *Clipboard Events* - *Drag Events* - *Focus Events* - *General Events* - *Input Events* - *Keyboard Events* - *Media Events* - *Mouse Events* - *Pointer Events* - *Progress Events* - *Touch Events* Um diese zu nutzen, muss das Element in der Komponente zuerst gefunden werden. Das wird normalerweise mit der `Find(string cssSelector)` Methode gemacht. ### Event Handler eines Elements ausführen Für dieses Beispiel nehmen wir eine Komponente mit einem `@onclick` Event Handler. ```csharp <button @onclick="OnClick">Foo Button</button> @code { void OnClick(MouseEventArgs args) { // ... } } ``` Um den Event Handler jetzt auszuführen, brauchen wir folgenden Code: ```csharp public class FooTest : TestContextWrapper { [Test] public void FooButton_PressButton() { // Arrange var cut = RenderComponent<FooButton>(); var buttonElement = cut.Find("button"); // Act buttonElement.Click(); buttonElement.Click(detail: 3, ctrlKey: true); buttonElement.Click(new MouseEventArgs()); // Assert // ... } } ``` ## Ergebnis Verifizieren Wenn eine Komponente in einem Test gerendert wird, erhält man entweder ein `IRenderedFragment` oder eine `IRenderedComponent<TComponent>`. Durch diese ist es möglich, das gerenderte Markup einer Komponente und im Fall von `IRenderedComponent<TComponent>` die Instanz der Komponente abzurufen. ### DOM Nodes überprüfen Das gerenderte Markup einer Komponente ist als DOM-Node über die `Nodes` Eigenschaft des `IRenderedFragment` verfügbar. bUnit bietet verschiedene Methoden an, um gerenderte HTML-Elemente zu suchen: - `FindByLabelText(string labelText)`, nimmt einen String Parameter welcher die Beschriftung eines Eingabefelds darstellt. Wenn kein Element gefunden wird, wirft sie eine *Exception*. Diese Methode ist in der experimentellen `bunit.web.query` Bibliothek verfügbar und sollte wenn möglich den anderen Optionen vorgezogen werden. - `Find(string cssSelector)` - `FindAll(string cssSelector)` Alle drei Methoden geben ein `IElement` Objekt zurück. Sobald man ein solches Element hat, überprüft man die Eigenschaften über die DOM-API. ### Semantische Vergleiche von Markup Mit rohem Markup zu arbeiten funktioniert nur mit sehr kleinem Output gut. Eine bessere Vorgehensweise ist der semantische HTML Vergleicher von bUnit. Beim HTML Vergleicher werden Leerzeichen und grundlegende Formatierung nicht beachtet. Der Vergleichern nimmt zwei HTML Fragmente als Input und gibt true zurück, wenn beide HTML Fragmente im gleichen optischen Output im Web Browser resultieren. Der HTML Vergleicher kann einfach über die `MarkupMatches` Methode erreicht werden. ```html <h1 id="heading-234" required> Heading Test <small> Secondary Text </small> </h1> ``` ```csharp cut.MarkupMatches(@"<h1 id=""heading-234"" required> Heading Text <small>Secondary Text</small> </h1> "); ``` Die `MarkupMatches` Methode kann auch einfach Texte vergleichen. Will man also beispielsweise den Text im `<small>` überprüfen, kann man das folgendermassen erreichen: ```csharp var smallElementText = cut.Find("small").TextContent; smallElementText.MarkupMatches("Secondary Text"); ``` [Die Methode kann auch personalisiert werden](https://bunit.dev/docs/verification/semantic-html-comparison.html), um die Testfälle noch stabiler und einfach zu machen. ### Erwartete Veränderung prüfen In einigen Szenarien ist es einfacher, nur die erwartete Veränderung zu prüfen, anstatt definieren zu müssen, wie das ganze gerenderte Markup aussehen sollte. bUnit kommt mit einigen verschiedenen Wegen, um die `IDiff` Listen zu finden: - `GetChangesSinceFirstRender()` - `GetChangesSinceSnapshot()` & `SaveSnapshot()`, diese beiden Methoden kombiniert ermöglichen es, eine Liste von Unterschieden zwischen dem letzten Aufruf von `SaveSnapshot()` und jetzt zu erhalten. - `CompareTo()`, gibt die Unterschiede zwischen zwei mitgegebenen HTML Fragmenten zurück Zu diesen gibt es auch noch viele experimentelle *Assertion Helpers* für `IDiff` und `IEnumerable<IDiff>`, welche es einfacher machen, die Vergleiche kurz zu fassen. Um uns einige dieser anzuschauen, werden wir eine `<Counter>` Komponente verwenden: ```csharp <h1>Counter</h1> <p> Current count: @currentCount </p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { int currentCount = 0; void IncrementCount() { currentCount++; } } ``` Hier ist ein Bespiel mit der `GetChangesSinceFirstRender()` Methode: ```csharp cut.Find("button").Click(); var diffs = cut.GetChangesSinceFirstRender(); var diff = diffs.ShouldHaveSingleChange(); diff.ShouldBeTextChange("Current count: 1"); ``` Es passiert folgendes: 1. Der Knopf wird gedrückt 2. `GetChangesSinceFirstRender()` gibt uns eine Liste mit allen Veränderungen 3. `ShouldHaveSingleChange()` überprüft, dass nur eine Änderung gefunden wurde 4. `ShouldBeTextChange()` überprüft, dass der einzelne `IDiff` eine Text Änderung ist Komponenten mit komplexeren Lebenszyklen können einfacher mit der `GetChangesSinceSnapshot()` Mehtode überprüft werden. ### Instanzen von Komponenten prüfen Mit dem `.Instance` Feld kann auf die Instanz selber zugegriffen werden. ```csharp IRenderedComponent<Counter> cut = RenderComponent<Counter>(); Counter counter = cut.Instance; ``` > [!warning] > Obwohl es möglich ist, bei einer solchen Instanz die `[Parameter]` und `[CascadingParameter]` Eigenschaften zu setzen, wird dadurch kein Rerender ausgeführt. Stattdesen sollte immer die `SetParametersAndRender()` Methode genutzt werden. ## Mocken ### Authentifizierung und Autorisierung bUnit kommt mit Test-spezifischen Implementationen von den Blazor Authentifizierungs und Autorisierungs Typen. Dadurch ist es einfach, `<AuthorizeView>`, `<CascadingAuthenticationState>` und `<AuthorizeRouteView>` Komponenten sowie den `AuthenticationStateProvider` Typen zu testen. Um die Implementation nutzen zu können, muss nur `AddTestAuthorization()` in einem Test Context aufgerufen werden. Dies fügt die nötigen `Services` *Collection* und `CascadingAuthenticationState` Komponente an den *Root Render Tree* an. Die Methode gibt eine Instanz des `TestAuthorizationContext` Typen zurück, welche genutzt werden kann, um den Authentifikations- und Autorisierungszustand kontrollieren zu können. > [!note] > Wenn die Test Klasse vom TextContext erbt, muss die Methode auf `this` aufgerufen werden: `this.AddTestAuthorization()`. #### Authentifiziert aber nicht autorisiert ```csharp var authContext = this.AddTestAuthorization(); authContext.SetAuthorized("TEST USER", AuthorizationState.Unauthorized); ``` #### Authentifiziert und autorisiert ```csharp var authContext = this.AddTestAuthorization(); authContext.SetAuthorized("TEST USER"); ``` #### Claims Es können auch Claims gesetzt werden: ```csharp var authContext = this.AddTestAuthorization(); authContext.SetAuthorized("TEST USER"); authContext.SetClaims( new Claim(ClaimTypes.Email, "[email protected]"), new Claim(ClaimTypes.DateOfBirth, "01-01-2000") ); ``` ## Weitere Tips Für die Projekt Struktur wird folgender Aufbau empfohlen: ``` src | MyComponentLib.csproj (namespace e.g. "Company.MyComponentLib") | _Imports.razor | Component1.razor | SubFolder | SubComponent1.razor test | MyComponentLibTests.csproj (with project reference to MyComponentLib.csproj) | _Imports.razor | Component1Test.cs | SubFolder | SubComponent1Test.cs ``` Es wird ausserdem empfohlen, den gleichen *Root Namespace* zu verwenden, um die Anzahl an Imports zu minimieren.