From 96928ccfbc0ce4fe1773a7fb55a74aef7266140b Mon Sep 17 00:00:00 2001 From: Patrick Pimentel Date: Thu, 22 Jan 2026 12:05:19 -0500 Subject: [PATCH] Move testing documentation to test/Common/TESTING.md - Create comprehensive TESTING.md with patterns, examples, and SutProvider usage - Simplify CLAUDE.md testing section to reference the new file - Keep quick reference in CLAUDE.md for common attributes Co-Authored-By: Claude Opus 4.5 --- .claude/CLAUDE.md | 104 +------------------- test/Common/TESTING.md | 209 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 214 insertions(+), 99 deletions(-) create mode 100644 test/Common/TESTING.md diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index d3b64a53e7..8dcd4f38db 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -81,106 +81,12 @@ Two ORM implementations exist for database flexibility: ## Testing Patterns -Tests use **xUnit** with **NSubstitute** for mocking and **AutoFixture** for test data. All tests must follow the **AAA (Arrange-Act-Assert)** pattern with clear section comments. +See **[test/Common/TESTING.md](../test/Common/TESTING.md)** for detailed testing patterns, examples, and SutProvider usage. -### Unit Tests -Unit tests mock dependencies and test isolated business logic: - -```csharp -[SutProviderCustomize] -public class DeleteGroupCommandTests -{ - [Theory, BitAutoData] - public async Task DeleteGroup_Success(SutProvider sutProvider, Group group) - { - // Arrange - sutProvider.GetDependency() - .GetByIdAsync(group.Id) - .Returns(group); - - // Act - await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id); - - // Assert - await sutProvider.GetDependency().Received(1).DeleteAsync(group); - await sutProvider.GetDependency().Received(1) - .LogGroupEventAsync(group, EventType.Group_Deleted); - } -} -``` - -Key testing utilities: -- `[BitAutoData]` - AutoFixture attribute for generating test data -- `SutProvider` - Helper for creating system-under-test with mocked dependencies -- `[SutProviderCustomize]` - Attribute to enable SutProvider pattern - -**SutProvider advanced usage:** -- **Parameter order with inline data**: `[BitAutoData("value")]` inline parameters come before `SutProvider` in the method signature -- **Non-mock dependencies**: Use `new SutProvider().SetDependency(realInstance).Create()` when you need a real implementation (e.g., `FakeLogger`) instead of a mock -- **Interface matching**: SutProvider matches dependencies by the exact interface type in the constructor - -### Integration Tests -Integration tests exercise real code paths with actual database operations. **Do not mock** - use real repositories and test helpers to set up data: - -```csharp -public class GroupRepositoryTests -{ - [DatabaseTheory, DatabaseData] - public async Task AddGroupUsersByIdAsync_CreatesGroupUsers( - IGroupRepository groupRepository, - IUserRepository userRepository, - IOrganizationUserRepository organizationUserRepository, - IOrganizationRepository organizationRepository) - { - // Arrange - var user1 = await userRepository.CreateTestUserAsync("user1"); - var user2 = await userRepository.CreateTestUserAsync("user2"); - var org = await organizationRepository.CreateTestOrganizationAsync(); - var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); - var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); - var group = await groupRepository.CreateTestGroupAsync(org); - - // Act - await groupRepository.AddGroupUsersByIdAsync(group.Id, [orgUser1.Id, orgUser2.Id]); - - // Assert - var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); - Assert.Equal(new[] { orgUser1.Id, orgUser2.Id }.Order(), actual.Order()); - } -} -``` - -API integration tests use `ApiApplicationFactory` and real HTTP calls: - -```csharp -public class OrganizationsControllerTests : IClassFixture, IAsyncLifetime -{ - [Fact] - public async Task Put_AsOwner_CanUpdateOrganization() - { - // Arrange - await _loginHelper.LoginAsync(_ownerEmail); - var updateRequest = new OrganizationUpdateRequestModel - { - Name = "Updated Organization Name", - BillingEmail = "newbilling@example.com" - }; - - // Act - var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var organizationRepository = _factory.GetService(); - var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id); - Assert.Equal("Updated Organization Name", updatedOrg.Name); - } -} -``` - -Key integration test attributes: -- `[DatabaseTheory, DatabaseData]` - For repository tests against real databases -- `IClassFixture` - For API controller tests +**Quick reference:** +- **Unit tests**: Use `[SutProviderCustomize]`, `[Theory, BitAutoData]`, and `SutProvider` for mocked dependencies +- **Integration tests**: Use `[DatabaseTheory, DatabaseData]` for repository tests, `ApiApplicationFactory` for API tests +- **All tests**: Follow AAA (Arrange-Act-Assert) pattern with clear section comments ## Critical Rules diff --git a/test/Common/TESTING.md b/test/Common/TESTING.md new file mode 100644 index 0000000000..77fbc31b5f --- /dev/null +++ b/test/Common/TESTING.md @@ -0,0 +1,209 @@ +# Testing Patterns + +This document describes testing patterns and infrastructure used in the Bitwarden server codebase. + +## Test Framework Stack + +- **xUnit** - Test framework +- **NSubstitute** - Mocking library +- **AutoFixture** - Test data generation +- **SutProvider** - Custom helper for creating system-under-test with mocked dependencies + +## AAA Pattern + +All tests must follow the **Arrange-Act-Assert** pattern with clear section comments: + +```csharp +[Theory, BitAutoData] +public async Task MethodName_Scenario_ExpectedResult(SutProvider sutProvider, Entity entity) +{ + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(entity.Id) + .Returns(entity); + + // Act + var result = await sutProvider.Sut.DoSomethingAsync(entity.Id); + + // Assert + Assert.NotNull(result); + await sutProvider.GetDependency().Received(1).GetByIdAsync(entity.Id); +} +``` + +## Unit Tests + +Unit tests mock dependencies and test isolated business logic. + +### Key Attributes and Utilities + +- `[SutProviderCustomize]` - Class-level attribute to enable SutProvider pattern +- `[Theory, BitAutoData]` - Generates test data via AutoFixture +- `SutProvider` - Creates the system-under-test with all dependencies mocked +- `sutProvider.Sut` - The instance being tested +- `sutProvider.GetDependency()` - Access mocked dependencies for setup or verification + +### Basic Example + +```csharp +[SutProviderCustomize] +public class DeleteGroupCommandTests +{ + [Theory, BitAutoData] + public async Task DeleteGroup_Success(SutProvider sutProvider, Group group) + { + // Arrange + sutProvider.GetDependency() + .GetByIdAsync(group.Id) + .Returns(group); + + // Act + await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id); + + // Assert + await sutProvider.GetDependency().Received(1).DeleteAsync(group); + await sutProvider.GetDependency().Received(1) + .LogGroupEventAsync(group, EventType.Group_Deleted); + } +} +``` + +### SutProvider Advanced Usage + +#### Parameter Order with Inline Data + +When using `[BitAutoData("value")]` with inline test data, the inline parameters come **before** `SutProvider` in the method signature: + +```csharp +[Theory] +[BitAutoData("password")] +[BitAutoData("webauthn")] +public async Task ValidateAsync_GrantTypes_ShouldWork( + string grantType, // Inline data first + SutProvider sutProvider, // Then SutProvider + User user) // Then AutoFixture-generated data +{ + // grantType will be "password" or "webauthn" +} +``` + +#### Non-Mock Dependencies + +By default, SutProvider creates NSubstitute mocks for all constructor dependencies. When you need a real implementation instead of a mock (e.g., `FakeLogger` to verify log output), use `SetDependency`: + +```csharp +[Theory, BitAutoData] +public async Task SomeTest_ShouldLogWarning(/* no SutProvider param - we create it manually */) +{ + // Arrange + var fakeLogger = new FakeLogger(); + var sutProvider = new SutProvider() + .SetDependency(fakeLogger) // Use real FakeLogger instead of mock + .Create(); + + // Act + await sutProvider.Sut.DoSomething(); + + // Assert + var logs = fakeLogger.Collector.GetSnapshot(); + Assert.Contains(logs, l => l.Level == LogLevel.Warning); +} +``` + +#### Interface Matching + +`SetDependency()` must match the **exact** interface type in the constructor: + +```csharp +// Constructor takes: ILogger logger (non-generic) +public MyService(ILogger logger, IRepository repo) { } + +// WRONG - ILogger won't match ILogger +sutProvider.SetDependency>(fakeLogger) // Ignored! + +// CORRECT - matches the constructor parameter type exactly +sutProvider.SetDependency(fakeLogger) // Works! +``` + +If the types don't match, SutProvider silently ignores the `SetDependency` call and creates a mock instead. + +## Integration Tests + +Integration tests exercise real code paths with actual database operations. **Do not mock** - use real repositories and test helpers to set up data. + +### Repository Integration Tests + +Use `[DatabaseTheory, DatabaseData]` for tests against real databases: + +```csharp +public class GroupRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task AddGroupUsersByIdAsync_CreatesGroupUsers( + IGroupRepository groupRepository, + IUserRepository userRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationRepository organizationRepository) + { + // Arrange + var user1 = await userRepository.CreateTestUserAsync("user1"); + var user2 = await userRepository.CreateTestUserAsync("user2"); + var org = await organizationRepository.CreateTestOrganizationAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user1); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(org, user2); + var group = await groupRepository.CreateTestGroupAsync(org); + + // Act + await groupRepository.AddGroupUsersByIdAsync(group.Id, [orgUser1.Id, orgUser2.Id]); + + // Assert + var actual = await groupRepository.GetManyUserIdsByIdAsync(group.Id); + Assert.Equal(new[] { orgUser1.Id, orgUser2.Id }.Order(), actual.Order()); + } +} +``` + +### API Integration Tests + +Use `ApiApplicationFactory` for HTTP-level integration tests: + +```csharp +public class OrganizationsControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + + public OrganizationsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + } + + [Fact] + public async Task Put_AsOwner_CanUpdateOrganization() + { + // Arrange + await _loginHelper.LoginAsync(_ownerEmail); + var updateRequest = new OrganizationUpdateRequestModel + { + Name = "Updated Organization Name", + BillingEmail = "newbilling@example.com" + }; + + // Act + var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var organizationRepository = _factory.GetService(); + var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id); + Assert.Equal("Updated Organization Name", updatedOrg.Name); + } +} +``` + +### Key Integration Test Attributes + +- `[DatabaseTheory, DatabaseData]` - For repository tests against real databases +- `IClassFixture` - For API controller tests +- `IAsyncLifetime` - For async setup/teardown