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:
@@ -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
209
test/Common/TESTING.md
Normal 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
|
||||
Reference in New Issue
Block a user