Skip to content

Commit

Permalink
Add docs about unit tests practices #1168
Browse files Browse the repository at this point in the history
  • Loading branch information
hocinehacherouf committed Sep 20, 2022
1 parent c112f22 commit c59c9a3
Show file tree
Hide file tree
Showing 3 changed files with 326 additions and 1 deletion.
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

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

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

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

### Test method

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

Example: For a method called GetProduct, the behaviour that we want to test is that it should return an exsiting project.
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 designed/cut into three steps:

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

These three parts are visually defined with comments so that unit tests can be humain readeable:

```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 IoT Hub Portal, we use the library [fluentassertions](https://github.com/fluentassertions/fluentassertions) on unit tests for natural/humain readeable assertions.

## Mock

A unit test should only test its dedicated layer. Any lower layer that requires/interact with external resources should be mocked to make sure that unit tests are idempotents.

!!! note
For example, we want to implement unit tests on a controller that requires three services. Each service depends on others services/repositories/http clients that require external resources like databases, APIs...
Any execution of unit tests that require on these external resources can be altered (not idempotent) because they depend on the uptime, the data of these resources.

On 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 Iot Hob Portal, we use the library [bUnit](https://bunit.dev/)

## How to unit test component

Let's say we have 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 testing on blazor component. Also it avoids code duplication unit tests 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 the configuration of the setup of the test class, you can start implementing unit tests.

Below an exemple of a a unit test that assert that the method GetProduct of the serivce ProductService
has been called after the component has been 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 usefull with Assertion of asynchronous changes: It will block and wait in a
test method until the provided assert 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 test on blazor components, you can interact/query rendered HTML DOM for find html elements (buttons, div...) 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 render elements.
Withing a unit test that uses these MudBlazor components, if the component `MudPopoverProvider` is not rendered, we will have limited interations with these components

Let's start with the example below:

```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 interact with the component `MudAutocomplete` to search 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

0 comments on commit c59c9a3

Please sign in to comment.