-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Add docs about unit tests practices #1168 * Fix typos on unit tests docs
- Loading branch information
1 parent
c112f22
commit b365c2f
Showing
3 changed files
with
326 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
201
docs/dev-guide/testing/unit-tests-on-blazor-components.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
... | ||
} | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters