Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add docs about unit tests practices #1168 #1272

Merged
merged 2 commits into from
Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions docs/dev-guide/testing/unit-tests-common-practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Unit Tests Common Practices

## Naming Conventions

### Test class

The test class should follow the naming convention **`[ClassUnderTest]Tests`**.

Example: The test class for a class named ProductController should be named `ProductControllerTests`:

```csharp
[TestFixture]
public class ProductControllerTests
{
...
}
```

### Test method

The test method should follow the naming convention **`[MethodUnderTest]_[BehaviourToTest]_[ExpectedResult]`**.

Example: A method named GetProduct should be tested to see if it returns an existing product.
The name of the test should be `GetProduct_ProductExist_ProductReturned`:

```csharp
[Test]
public async Task GetProduct_ProductExist_ProductReturned()
{
...
}
```

## Unit Test Skeleton: Three Steps/Parts

A unit test should be devided into three steps:

1. Arrange: The first part where the input/expected data are defined
2. Act: The second part where the behavior under test is executed
3. Assert: The third and final part where assertions are made

These three parts are visually defined with comments so that unit tests are humanly comprehensible:

```csharp
[Test]
public async Task GetProduct_ProductExist_ProductReturned()
{
// Arrange
var productId = Guid.NewGuid().ToString();
var expectedProduct = new Product
{
Id = productId
};

// Act
var product = this.productService.GetProduct(productId);

// Asset
_ = product.Should().BeEquivalentTo(expectedProduct);
}
```

!!! Tip
On the IoT Hub portal, we use the [fluentassertions](https://github.com/fluentassertions/fluentassertions) library for unit tests for natural/human reusable assertions.

## Mock

A unit test should only test its assigned layer. Any lower layer that requires/interacts with external resources should be mocked to ensure sure that the unit tests are idempotent.

!!! note
Example: We want to implement unit tests for a controller that requires three services. Each service depends on other services/repositories/http clients that need external resources like databases, APIs...
Any execution of unit tests that depend on these external resources can be altered (not idempotent) because they depend on the uptime and data of these resources.

On the IoT Hub portal, we use the library [Moq](https://github.com/moq/moq4) for mocking within unit tests:

```csharp
[TestFixture]
public class ProductControllerTests
{
private MockRepository mockRepository;
private Mock<IProductRepository> mockProductRepository;

private IProductService productService;

[SetUp]
public void SetUp()
{
// Init MockRepository with strict behaviour
this.mockRepository = new MockRepository(MockBehavior.Strict);
// Init the mock of IProductRepository
this.mockProductRepository = this.mockRepository.Create<IProductRepository>();
// Init the service ProductService. The object mock ProductRepository is passed the contructor of ProductService
this.productService = new ProductService(this.mockProductRepository.Object);
}

[Test]
public async Task GetProduct_ProductExist_ProductReturned()
{
// Arrange
var productId = Guid.NewGuid().ToString();
var expectedProduct = new Product
{
Id = productId
};

// Setup mock of GetByIdAsync of the repository ProductRepository to return the expected product when given the correct product id
_ = this.mockProductRepository.Setup(repository => repository.GetByIdAsync(productId))
.ReturnsAsync(expectedProduct);

// Act
var product = this.productService.GetProduct(productId);

// Asset
_ = product.Should().BeEquivalentTo(expectedProduct);

// Assert that all mocks setups have been called
_ = MockRepository.VerifyAll();
}
}
```
201 changes: 201 additions & 0 deletions docs/dev-guide/testing/unit-tests-on-blazor-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Unit Tests on Blazor components

!!! info
To test Blazor components on the Iot Hob Portal, we use the library [bUnit](https://bunit.dev/)

## How to unit test component

Let us assume we have a compoment `ProductDetail` to test.

```csharp title="Example of the content of the component ProductDetail"
@inject IProductService ProductService

@if(product != null)
{
<p id="product-id">@product.Id</p>
}

@code {
[Parameter]
public string ProductId { get; set; }

private Product product;

protected override async Task OnInitializedAsync()
{
await GetProduct();
}

private async Task GetProduct()
{
try
{
product = await ProductService.GetProduct(ProductId);
}
catch (ProblemDetailsException exception)
{
Error?.ProcessProblemDetails(exception);
}
}
}
```

```csharp title="First you have to a unit test class that extend"
[TestFixture]
public class ProductDetailTests : BlazorUnitTest
{
}
```

!!! info
The class [`BlazorUnitTest`](https://github.com/CGI-FR/IoT-Hub-Portal/blob/main/src/AzureIoTHub.Portal.Tests.Unit/UnitTests/Bases/BlazorUnitTest.cs)
provides helpers/test context dedicated for unit tests for the blazor component. It also avoids code duplication of unit test classes.

```csharp title="Override the method Setup"
[TestFixture]
public class ProductDetailTests : BlazorUnitTest
{
public override void Setup()
{
// Don't forget the method base.Setup() to initialize existing helpers
base.Setup();
}
}
```

```csharp title="Setup the mockup of the service IProductService"
[TestFixture]
public class ProductDetailTests : BlazorUnitTest
{
// Declare the mock of IProductService
private Mock<IProductService> productServiceMock;

public override void Setup()
{
base.Setup();

// Intialize the mock of IProductService
this.productServiceMock = MockRepository.Create<IProductService>();

// Add the mock of IProductService as a singleton for resolution
_ = Services.AddSingleton(this.productServiceMock.Object);
}
}
```

!!! Info
After configuring the test class setup, you can start implementing unit tests.

Below is an example of a a unit test that checks whether the GetProduct method of the serivce ProductService service
was called after the component was initialized:

```csharp
[TestFixture]
public class ProductDetailTests : BlazorUnitTest
{
...

[Test]
public void OnInitializedAsync_GetProduct_ProductIsRetrieved()
{
// Arrange
var expectedProduct = Fixture.Create<Product>();

// Setup mock of GetProduct of the service ProductService
_ = this.productServiceMock.Setup(service => service.GetProduct(expectedProduct.Id))
.ReturnsAsync(expectedProduct);

// Act
// Render the component ProductDetail with the required ProductId parameter
var cut = RenderComponent<ProductDetail>(ComponentParameter.CreateParameter("ProductId", expectedProduct.Id));
// You can wait for a specific element to be rendered before assertions using a css selector, for example the DOM element with id product-id
_ = cut.WaitForElement("#product-id");

// Assert
// Assert that all mocks setups have been called
cut.WaitForAssertion(() => MockRepository.VerifyAll());
}
}
```

!!! Tip
`WaitForAssertion` is useful in asserting asynchronous changes: It will blocks and waits in a
test method until the specified assertion action does not throw an exception, or until the timeout is reached (the default
timeout is one second). :point_right: [Assertion of asynchronous changes](https://bunit.dev/docs/verification/async-assertion.html)

!!! Tip
Within unit tests on Blazor components, you can interact with HTML DOM and query rendered HTMLelements (buttons, div...) by using
CSS selectors (id, class...) :point_right: Lean more about [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)

## How to unit test a component requiring an external component

Some components proposed by MudBlazor (MudAutocomplete, MudSelect...) use another component `MudPopoverProvider` to display elements.
If in a unit test that uses these MudBlazor components, the `MudPopoverProvider` component is not rendered, the interactions with these components are restricted.

Let us start with the following example:

```csharp title="Example of the content of the component SearchState"
<MudAutocomplete T="string" Label="US States" @bind-Value="selectedState" SearchFunc="@Search" />

@code {
private string selectedState;
private string[] states =
{
"Alabama", "Colorado", "Missouri", "Wisconsin"
}

private async Task<IEnumerable<string>> Search(string value)
{
// In real life use an asynchronous function for fetching data from an api.
await Task.Delay(5);

// if text is null or empty, show complete list
if (string.IsNullOrEmpty(value))
return states;
return states.Where(x => x.Contains(value, StringComparison.InvariantCultureIgnoreCase));
}
}
```

We want to test the search when a user interacts with the `MudAutocomplete` component to search for the state `Wisconsin`:

```csharp
[TestFixture]
public class SearchStateTests : BlazorUnitTest
{
...

[Test]
public void Search_UserSearchAndSelectState_StateIsSelected()
{
// Arrange
var userQuery = "Wis";

// First render MudPopoverProvider component
var popoverProvider = RenderComponent<MudPopoverProvider>();
// Second, rendrer the component SearchState (under unit test)
var cut = RenderComponent<SearchState>();

// Find the MudAutocomplete component within SearchState component
var autocompleteComponent = cut.FindComponent<MudAutocomplete<string>>();

// Fire click event on,
autocompleteComponent.Find("input").Click();
autocompleteComponent.Find("input").Input(userQuery);

// Wait until the count of element in the list rendred on the component MudPopoverProvider is equals to one
popoverProvider.WaitForAssertion(() => popoverProvider.FindAll("div.mud-list-item").Count.Should().Be(1));

// Act
// Get the only element present on the list
var stateElement = popoverProvider.Find("div.mud-list-item");
// Fire click event on the element
stateElement.Click();

// Assert
// Check if the MudAutocomplete compoment has been closed after the click event
cut.WaitForAssertion(() => autocompleteComponent.Instance.IsOpen.Should().BeFalse());
...
}
}
```
6 changes: 5 additions & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,11 @@ extra:
# Navigation
nav:
- index.md
- Dev Guide: 'dev-guide.md'
- Dev Guide:
- 'Developer Guide': 'dev-guide.md'
- 'Testing':
- 'dev-guide/testing/unit-tests-common-practices.md'
- 'dev-guide/testing/unit-tests-on-blazor-components.md'
- Concepts: 'concepts.md'
- Web API Reference: 'open-api.md'
- About:
Expand Down