Appearance
Are you an LLM? You can read better optimized documentation at /testing/unit_testing.md for this page in Markdown format
Unit testing
This document provides a comprehensive guide to the unit testing strategy, patterns, and implementation details for TheExampleApp. A robust testing suite is crucial for maintaining code quality, preventing regressions, and enabling safe refactoring. The tests are built using xUnit as the test runner and Moq for creating mock objects.
For a general overview of testing in this project, see the Testing Overview.
Unit test structure
The unit tests for the application reside in the TheExampleApp.Tests project. The test project mirrors the structure of the main application (TheExampleApp) to ensure that tests are easy to locate and maintain. For example, a test for a view component located at TheExampleApp/ViewComponents/LocalesViewComponent.cs will be found at TheExampleApp.Tests/ViewComponents/LocalesViewComponentTests.cs.
All tests follow the widely-accepted Arrange-Act-Assert (AAA) pattern:
- Arrange: Initialize objects, create mock dependencies, and set up the test context. This phase prepares everything required for the test scenario.
- Act: Execute the method or function being tested. This is typically a single line of code.
- Assert: Verify that the outcome of the action is as expected. This involves checking return values, the state of objects, or whether mock methods were called with the correct parameters.
Test organization
Tests are organized by the type of component they target. This separation of concerns makes the test suite easier to navigate and understand.
Configuration tests
These tests validate custom configuration logic and middleware. Key examples include:
BreadcrumbsTests: Verifies theBreadcrumbsmiddleware, which dynamically generates a breadcrumb trail from the request URL. It tests path parsing, label formatting (e.g., replacing hyphens with spaces), and localization of breadcrumb labels.ContentfulOptionsManagerTests: Ensures that theContentfulOptionsManagercorrectly retrieves Contentful API credentials. It tests the logic that prioritizes credentials stored in the user's session over the default credentials fromappsettings.json, allowing for dynamic switching of Contentful spaces.
Page model tests
These tests focus on the logic within Razor Pages page models. The primary responsibility of a page model is to fetch data and prepare it for the view. Tests for page models typically involve:
- Mocking the
IContentfulClientto return a predefined set of entries. - Instantiating the page model class with the mocked client.
- Calling the handler method (e.g.,
OnGet()). - Asserting that the public properties on the model (which the view will bind to) are populated with the expected data.
The IndexModelTests.cs file provides a clear example of this pattern.
View component tests
Tests for View Components verify the logic that prepares data for a partial view. Since view components can have dependencies and access the HttpContext, testing them requires setting up a mock ViewComponentContext.
The LocalesViewComponentTests.cs demonstrates how to test a view component that interacts with both the IContentfulClient (to get available locales) and the ISession (to persist the user's selected locale). It includes tests for success paths, fallback logic (e.g., selecting a default locale), and error handling when the Contentful API is unavailable.
Tag helper tests
Tests for Tag Helpers validate custom HTML generation logic. These tests ensure that the helpers render the correct output based on their attributes and child content.
The MarkdownTagHelperTests.cs shows how to test the MarkdownTagHelper. It verifies that Markdown content is correctly converted to HTML, both when using a custom <markdown> tag and when using the markdown attribute on a standard HTML element like <div>.
Testing patterns
The following patterns are consistently used across the test suite to ensure effective and maintainable tests.
Mocking dependencies
Dependencies are mocked using the Moq library to isolate the unit of code being tested. This prevents tests from relying on external systems like the Contentful API or a database.
Commonly mocked interfaces include:
IContentfulClient: To simulate responses from the Contentful API.IHttpContextAccessor,HttpContext,HttpRequest,ISession: To simulate the web request context and session state.IViewLocalizer: To test internationalization (i18n) logic without needing actual resource files.
The following snippet from ContentfulOptionsManagerTests.cs shows how to mock HttpContext and ISession to simulate reading configuration from the session.
csharp
// Arrange
var mockOptions = new Mock<IOptions<ContentfulOptions>>();
mockOptions.Setup(x => x.Value).Returns(new ContentfulOptions() { DeliveryApiKey = "brunsås" });
var mockSession = new Mock<ISession>();
// Simulate finding a value in the session
byte[] dummy = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new ContentfulOptions { DeliveryApiKey = "Schönebrunn" }));
mockSession.Setup(x => x.TryGetValue(nameof(ContentfulOptions), out dummy)).Returns(true);
var mockContext = new Mock<HttpContext>();
mockContext.SetupGet(c => c.Session).Returns(mockSession.Object);
var httpContextAccessor = new Mock<IHttpContextAccessor>();
httpContextAccessor.SetupGet(c => c.HttpContext).Returns(mockContext.Object);
var manager = new ContentfulOptionsManager(mockOptions.Object, httpContextAccessor.Object);
// Act
var options = manager.Options;
// Assert
Assert.Equal("Schönebrunn", options.DeliveryApiKey);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Testing Razor Pages
To test a Razor Page model, you instantiate it directly, inject its mocked dependencies, and invoke its handler methods.
csharp
// From: TheExampleApp.Tests/Pages/IndexModelTests.cs
[Fact]
public async Task GettingIndexShouldSetLayoutCorrectly()
{
// Arrange: Create a mock collection of Layout objects
var collection = new ContentfulCollection<Layout>();
collection.Items = new List<Layout>()
{
new Layout()
{
Title = "SomeTitle"
}
};
// Arrange: Mock the Contentful client to return the mock collection
var client = new Mock<IContentfulClient>();
client.SetupGet(c => c.SerializerSettings).Returns(new JsonSerializerSettings());
client.Setup(c => c.GetEntries(It.IsAny<QueryBuilder<Layout>>(), default(CancellationToken)))
.Returns(Task.FromResult(collection));
var model = new IndexModel(client.Object);
// Act: Call the page handler
await model.OnGet();
// Assert: Verify the model's property was set correctly
Assert.Equal("SomeTitle", model.IndexPage.Title);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Testing view components
Testing a view component requires creating a ViewComponentContext to provide it with a simulated HttpContext. This allows you to test logic that depends on session state or request data.
csharp
// From: TheExampleApp.Tests/ViewComponents/LocalesViewComponentTests.cs
[Fact]
public async Task ComponentShouldGetTheSelectedLocale()
{
// Arrange: Mock the Contentful client and its response
var space = new Space { /* ... locales ... */ };
var client = new Mock<IContentfulClient>();
client.Setup(c => c.GetSpace(default(CancellationToken))).Returns(Task.FromResult(space));
// Arrange: Mock the session and HttpContext
var mockSession = new Mock<ISession>();
mockSession.Setup(x => x.Set("locale", It.IsAny<byte[]>())).Verifiable();
var httpContext = new Mock<HttpContext>();
httpContext.SetupGet(c => c.Session).Returns(mockSession.Object);
// Arrange: Create the ViewComponentContext
var viewContext = new ViewContext { HttpContext = httpContext.Object };
var componentContext = new ViewComponentContext { ViewContext = viewContext };
var component = new LocalesViewComponent(client.Object);
component.ViewComponentContext = componentContext;
// Act
var res = await component.InvokeAsync();
// Assert: Check the result type and the model data
Assert.IsType<ViewViewComponentResult>(res);
Assert.Equal("sv-SE", ((res as ViewViewComponentResult).ViewData.Model as LocalesInfo).SelectedLocale.Code);
// Assert: Verify that the session was updated
mockSession.Verify(x => x.Set("locale", It.Is<byte[]>(b => Encoding.UTF8.GetString(b) == "sv-SE")), Times.Once);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Assertion strategies
The test suite uses xUnit's rich assertion library. Beyond simple equality checks (Assert.Equal), more advanced assertions are used to create expressive and robust tests.
A key example is Assert.Collection, which verifies every element in a collection against a series of Action<T> delegates. This is particularly useful for testing ordered lists, such as the breadcrumb trail.
csharp
// From: TheExampleApp.Tests/Configuration/BreadcrumbsTests.cs
[Fact]
public async Task BreadCrumbsShouldGetAllPartsOfPath()
{
// ... Arrange ...
var mockContext = new Mock<HttpContext>();
var mockRequest = new Mock<HttpRequest>();
mockRequest.SetupGet(r => r.Path).Returns(new PathString("/Courses/Course-X/Lessons/Lesson-Y"));
mockContext.SetupGet(c => c.Request).Returns(mockRequest.Object);
mockContext.SetupGet(c => c.Items).Returns(new Dictionary<object, object>());
// ... Act ...
await breadcrumbs.Invoke(mockContext.Object);
// Assert
Assert.True(mockContext.Object.Items.ContainsKey("breadcrumbs"));
Assert.Collection((mockContext.Object.Items["breadcrumbs"] as List<Breadcrumb>),
// Assert the properties of the first breadcrumb
(b) => {
Assert.Equal("Home", b.Label);
Assert.Equal("/", b.Path);
},
// Assert the properties of the second breadcrumb
(b) => {
Assert.Equal("Courses", b.Label);
Assert.Equal("/Courses", b.Path);
},
// ... and so on for each part of the path
(b) => {
Assert.Equal("Course X", b.Label);
Assert.Equal("/Courses/Course-X", b.Path);
},
(b) => {
Assert.Equal("Lessons", b.Label);
Assert.Equal("/Courses/Course-X/Lessons", b.Path);
},
(b) => {
Assert.Equal("Lesson Y", b.Label);
Assert.Equal("/Courses/Course-X/Lessons/Lesson-Y", b.Path);
}
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Example tests
Below are selected full test classes that exemplify the patterns discussed above.
MarkdownTagHelperTests
This test demonstrates how to unit test a TagHelper. It involves creating a TagHelperContext and TagHelperOutput, invoking the ProcessAsync method, and asserting the final rendered content.
csharp
// From: TheExampleApp.Tests/TagHelpers/MarkdownTagHelperTests.cs
public class MarkdownTagHelperTests
{
// Helper to create TagHelperContent from a string
private Func<bool, HtmlEncoder, Task<TagHelperContent>> GetChildContent(string childContent)
{
var content = new DefaultTagHelperContent();
var tagHelperContent = content.SetContent(childContent);
return (b, encoder) => Task.FromResult(tagHelperContent);
}
[Fact]
public async Task MarkdownTagWithChildContentShouldReturnRenderedMarkdown()
{
//Arrange
var helper = new MarkdownTagHelper();
var context = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString());
var output = new TagHelperOutput("markdown", new TagHelperAttributeList(), GetChildContent("## Banana"));
//Act
await helper.ProcessAsync(context, output);
//Assert
Assert.Null(output.TagName); // The <markdown> tag itself should be removed
Assert.StartsWith("<h2>Banana</h2>", output.Content.GetContent());
}
[Fact]
public async Task DivWithAttributeShouldReturnRenderedMarkdown()
{
//Arrange
var helper = new MarkdownTagHelper();
var context = new TagHelperContext(new TagHelperAttributeList { new TagHelperAttribute("markdown") }, new Dictionary<object, object>(), Guid.NewGuid().ToString());
var output = new TagHelperOutput("div", new TagHelperAttributeList { new TagHelperAttribute("markdown") }, GetChildContent("# Mr French"));
//Act
await helper.ProcessAsync(context, output);
//Assert
Assert.Equal("div", output.TagName); // The <div> tag should be preserved
Assert.StartsWith("<h1>Mr French</h1>", output.Content.GetContent());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43