# 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.