diff --git a/docs/dev-guide/testing/unit-tests-common-practices.md b/docs/dev-guide/testing/unit-tests-common-practices.md new file mode 100644 index 000000000..05255dae7 --- /dev/null +++ b/docs/dev-guide/testing/unit-tests-common-practices.md @@ -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 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(); + // 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(); + } +} +``` diff --git a/docs/dev-guide/testing/unit-tests-on-blazor-components.md b/docs/dev-guide/testing/unit-tests-on-blazor-components.md new file mode 100644 index 000000000..fab417b0d --- /dev/null +++ b/docs/dev-guide/testing/unit-tests-on-blazor-components.md @@ -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) +{ +

@product.Id

+} + +@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 productServiceMock; + + public override void Setup() + { + base.Setup(); + + // Intialize the mock of IProductService + this.productServiceMock = MockRepository.Create(); + + // 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(); + + // 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(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" + + +@code { + private string selectedState; + private string[] states = + { + "Alabama", "Colorado", "Missouri", "Wisconsin" + } + + private async Task> 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(); + // Second, rendrer the component SearchState (under unit test) + var cut = RenderComponent(); + + // Find the MudAutocomplete component within SearchState component + var autocompleteComponent = cut.FindComponent>(); + + // 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()); + ... + } +} +``` diff --git a/mkdocs.yml b/mkdocs.yml index 5a911704e..86bce3fa6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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: