microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.2.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

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

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