1
0
mirror of https://github.com/bitwarden/server synced 2026-01-26 14:23:21 +00:00

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 <noreply@anthropic.com>
This commit is contained in:
Patrick Pimentel
2026-01-22 12:05:19 -05:00
parent f5b0c3aa2b
commit 96928ccfbc
2 changed files with 214 additions and 99 deletions

View File

@@ -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<DeleteGroupCommand> sutProvider, Group group)
{
// Arrange
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id)
.Returns(group);
// Act
await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id);
// Assert
await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);
await sutProvider.GetDependency<IEventService>().Received(1)
.LogGroupEventAsync(group, EventType.Group_Deleted);
}
}
```
Key testing utilities:
- `[BitAutoData]` - AutoFixture attribute for generating test data
- `SutProvider<T>` - 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<T>` in the method signature
- **Non-mock dependencies**: Use `new SutProvider<T>().SetDependency<IInterface>(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<ApiApplicationFactory>, 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<IOrganizationRepository>();
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<ApiApplicationFactory>` - For API controller tests
**Quick reference:**
- **Unit tests**: Use `[SutProviderCustomize]`, `[Theory, BitAutoData]`, and `SutProvider<T>` 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

209
test/Common/TESTING.md Normal file
View File

@@ -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<MyService> sutProvider, Entity entity)
{
// Arrange
sutProvider.GetDependency<IRepository>()
.GetByIdAsync(entity.Id)
.Returns(entity);
// Act
var result = await sutProvider.Sut.DoSomethingAsync(entity.Id);
// Assert
Assert.NotNull(result);
await sutProvider.GetDependency<IRepository>().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<T>` - Creates the system-under-test with all dependencies mocked
- `sutProvider.Sut` - The instance being tested
- `sutProvider.GetDependency<TInterface>()` - Access mocked dependencies for setup or verification
### Basic Example
```csharp
[SutProviderCustomize]
public class DeleteGroupCommandTests
{
[Theory, BitAutoData]
public async Task DeleteGroup_Success(SutProvider<DeleteGroupCommand> sutProvider, Group group)
{
// Arrange
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id)
.Returns(group);
// Act
await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id);
// Assert
await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);
await sutProvider.GetDependency<IEventService>().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<T>` in the method signature:
```csharp
[Theory]
[BitAutoData("password")]
[BitAutoData("webauthn")]
public async Task ValidateAsync_GrantTypes_ShouldWork(
string grantType, // Inline data first
SutProvider<MyValidator> 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<MyTests>();
var sutProvider = new SutProvider<MyService>()
.SetDependency<ILogger>(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<TInterface>()` must match the **exact** interface type in the constructor:
```csharp
// Constructor takes: ILogger logger (non-generic)
public MyService(ILogger logger, IRepository repo) { }
// WRONG - ILogger<T> won't match ILogger
sutProvider.SetDependency<ILogger<MyTests>>(fakeLogger) // Ignored!
// CORRECT - matches the constructor parameter type exactly
sutProvider.SetDependency<ILogger>(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<ApiApplicationFactory>, 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<IOrganizationRepository>();
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<ApiApplicationFactory>` - For API controller tests
- `IAsyncLifetime` - For async setup/teardown