microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.3.2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

.github/instructions/csharp/csharp-tests.instructions.md

137lines · modecode

1---
2applyTo: '**/*.cs'
3description: 'Required instructions for C# (CSharp) test code research, planning, implementation, editing, or creating - Brought to you by microsoft/hve-core'
4---
5
6# C# Test Instructions
7
8Conventions for C# test code. All conventions from [csharp.instructions.md](csharp.instructions.md) apply, including member ordering and field naming with underscore prefix.
9
10## Test Framework
11
12Use XUnit with NSubstitute for mocking. Focus on class behaviors rather than implementation details. Follow BDD-style naming and Arrange/Act/Assert structure.
13
14### Mocking Libraries
15
16| Library | Usage |
17|-------------|---------------------------------------------------|
18| NSubstitute | Preferred for new projects |
19| FakeItEasy | Acceptable alternative |
20| Moq | Existing projects only (pin to 4.18.x or 4.20.2+) |
21
22## Test Naming
23
24Test file naming matches the class under test: `PipelineServiceTests`.
25
26Test method format: `GivenContext_WhenAction_ExpectedResult`
27
28```text
29WhenValidRequest_ProcessDataAsync_ReturnsParsedResponse
30GivenEmptyInput_ProcessDataAsync_ThrowsArgumentException
31```
32
33Prefer one assertion per test. Related assertions validating the same behavior are acceptable. Do not verify logger mocks.
34
35Use `[Theory]` with `[InlineData]` for simple parameterized cases or `[MemberData]` for complex test data.
36
37## Test Organization
38
39* Fields at class top, alphabetically by name after underscore (`_httpClient` before `_sut`), `readonly` when possible
40* Service under test named `_sut`
41* Utility methods after constructor, before test methods
42* Test methods grouped by behavior, alphabetically within groups
43* Common mock setup in constructor; specific setup in test methods
44
45## NSubstitute Patterns
46
47Common mocking patterns:
48
49```csharp
50// Create substitutes
51var service = Substitute.For<IDataService>();
52var options = Substitute.For<IOptions<Config>>();
53
54// Configure returns
55service.GetAsync(Arg.Any<int>()).Returns(Task.FromResult(data));
56options.Value.Returns(new Config { Endpoint = "https://api.test" });
57
58// Argument matching
59service.Process(Arg.Is<Request>(r => r.Id > 0)).Returns(result);
60
61// Verify calls
62await service.Received(1).SaveAsync(Arg.Any<Data>());
63service.DidNotReceive().Delete(Arg.Any<int>());
64```
65
66## Lifecycle Interfaces
67
68Implement `IAsyncLifetime` for per-test setup and teardown:
69
70* `InitializeAsync` runs before each test
71* `DisposeAsync` runs after each test
72
73## Base Classes
74
75Create base classes when multiple test classes share setup logic. Name base class `*TestsBase` and derived class `ClassUnderTest_GivenContext` or `ClassUnderTest_WhenAction`. Define fake classes once in the base class.
76
77## Complete Example
78
79Using NSubstitute:
80
81```csharp
82public class EndpointDataProcessorTests
83{
84 private readonly HttpClient _httpClient;
85 private readonly MockHttpMessageHandler _httpHandler = new();
86 private readonly IOptions<PipelineOptions> _options;
87 private readonly EndpointDataProcessor<FakeSource, FakeSink> _sut;
88
89 public EndpointDataProcessorTests()
90 {
91 _options = Substitute.For<IOptions<PipelineOptions>>();
92 _options.Value.Returns(new PipelineOptions { EndpointUri = "https://test.com/predict" });
93
94 _httpClient = new HttpClient(_httpHandler);
95 _sut = new EndpointDataProcessor<FakeSource, FakeSink>(_options, _httpClient);
96 }
97
98 [Fact]
99 public async Task WhenValidRequest_ProcessDataAsync_ReturnsParsedResponse()
100 {
101 // Arrange
102 var expected = new FakeSink { Result = "Processed", Score = 0.95 };
103 _httpHandler.Response = new HttpResponseMessage(HttpStatusCode.OK)
104 {
105 Content = new StringContent(JsonSerializer.Serialize(expected))
106 };
107
108 // Act
109 var actual = await _sut.ProcessDataAsync(new FakeSource { Id = 1 }, CancellationToken.None);
110
111 // Assert
112 Assert.NotNull(actual);
113 Assert.Equivalent(expected, actual);
114 }
115
116 [Fact]
117 public async Task WhenServerError_ProcessDataAsync_ThrowsHttpRequestException()
118 {
119 // Arrange
120 _httpHandler.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
121
122 // Act & Assert
123 await Assert.ThrowsAsync<HttpRequestException>(
124 () => _sut.ProcessDataAsync(new FakeSource { Id = 1 }, CancellationToken.None));
125 }
126
127 public record FakeSource { public int Id { get; init; } }
128 public record FakeSink { public string? Result { get; init; } public double Score { get; init; } }
129
130 private class MockHttpMessageHandler : HttpMessageHandler
131 {
132 public HttpResponseMessage Response { get; set; } = new(HttpStatusCode.OK);
133 protected override Task<HttpResponseMessage> SendAsync(
134 HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(Response);
135 }
136}
137```
138