dotnet-testing-advanced-testcontainers-nosql
13
总安装量
11
周安装量
#25151
全站排名
安装命令
npx skills add https://github.com/kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-advanced-testcontainers-nosql
Agent 安装分布
claude-code
9
gemini-cli
8
antigravity
8
opencode
6
windsurf
5
github-copilot
5
Skill 文档
Testcontainers NoSQL æ´å測試æå
é©ç¨æ å¢
ç¶è¢«è¦æ±å·è¡ä»¥ä¸ä»»åæï¼è«ä½¿ç¨æ¤æè½ï¼
- ä½¿ç¨ Testcontainers 測試 MongoDB æä»¶æä½
- ä½¿ç¨ Testcontainers 測試 Redis å¿«åæå
- å»ºç« MongoDB Collection Fixture å ±äº«å®¹å¨
- å»ºç« Redis Collection Fixture å ±äº«å®¹å¨
- 測試 MongoDB BSON åºååèè¤éæä»¶çµæ§
- 測試 MongoDB ç´¢å¼æè½èå¯ä¸æ§ç´æ
- 測試 Redis äºç¨®è³æçµæ§ï¼StringãHashãListãSetãSorted Setï¼
- å¯¦ä½ NoSQL è³æåº«çè³æéé¢çç¥
æ ¸å¿æ¦å¿µ
NoSQL æ¸¬è©¦çææ°
NoSQL è³æåº«æ¸¬è©¦èéè¯å¼è³æåº«æé¡¯èå·®ç°ï¼
- æä»¶æ¨¡åè¤é度ï¼MongoDB æ¯æ´å·¢çç©ä»¶ãé£åãåå ¸çè¤éçµæ§
- ç¡åºå® Schemaï¼éè¦é鿏¬è©¦é©èè³æçµæ§çä¸è´æ§
- 夿¨£åè³æçµæ§ï¼Redis æäºç¨®ä¸»è¦è³æçµæ§ï¼åæä¸å使ç¨å ´æ¯
- åºååèçï¼BSON (MongoDB) è JSON (Redis) åºååè¡çºéè¦é©è
Testcontainers åªå¢
- ç實ç°å¢æ¨¡æ¬ï¼ä½¿ç¨å¯¦éç MongoDB 7.0 å Redis 7.2 容å¨
- ä¸è´æ§æ¸¬è©¦ï¼æ¸¬è©¦çµæç´æ¥åæ æ£å¼ç°å¢è¡çº
- é颿§ä¿èï¼æ¯å測試ç°å¢å®å ¨ç¨ç«
- æè½é©èï¼å¯é²è¡ç實çç´¢å¼æè½æ¸¬è©¦
ç°å¢éæ±
å¿ è¦å¥ä»¶
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- MongoDB ç¸éå¥ä»¶ -->
<PackageReference Include="MongoDB.Driver" Version="3.0.0" />
<PackageReference Include="MongoDB.Bson" Version="3.0.0" />
<!-- Redis ç¸éå¥ä»¶ -->
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
<!-- Testcontainers -->
<PackageReference Include="Testcontainers" Version="4.0.0" />
<PackageReference Include="Testcontainers.MongoDb" Version="4.0.0" />
<PackageReference Include="Testcontainers.Redis" Version="4.0.0" />
<!-- æ¸¬è©¦æ¡æ¶ -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<!-- JSON åºååèæé測試 -->
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.Bcl.TimeProvider" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
</ItemGroup>
</Project>
å¥ä»¶çæ¬èªªæ
| å¥ä»¶ | çæ¬ | ç¨é |
|---|---|---|
| MongoDB.Driver | 3.0.0 | MongoDB 宿¹é© åç¨å¼ï¼æ¯æ´ææ°åè½ |
| MongoDB.Bson | 3.0.0 | BSON åºååèç |
| StackExchange.Redis | 2.8.16 | Redis 客æ¶ç«¯ï¼æ¯æ´ Redis 7.x |
| Testcontainers.MongoDb | 4.0.0 | MongoDB 容å¨ç®¡ç |
| Testcontainers.Redis | 4.0.0 | Redis 容å¨ç®¡ç |
MongoDB 容å¨å測試
MongoDB Container Fixture
ä½¿ç¨ Collection Fixture 模å¼å ±äº«å®¹å¨ï¼ç¯ç 80% 以ä¸ç測試æéï¼
using MongoDB.Driver;
using Testcontainers.MongoDb;
namespace YourProject.Integration.Tests.Fixtures;
/// <summary>
/// MongoDB å®¹å¨ Fixture - å¯¦ä½ IAsyncLifetime 管ç容å¨çå½é±æ
/// </summary>
public class MongoDbContainerFixture : IAsyncLifetime
{
private MongoDbContainer? _container;
public IMongoDatabase Database { get; private set; } = null!;
public string ConnectionString { get; private set; } = string.Empty;
public string DatabaseName { get; } = "testdb";
public async Task InitializeAsync()
{
// ä½¿ç¨ MongoDB 7.0 確ä¿åè½å®æ´æ§
_container = new MongoDbBuilder()
.WithImage("mongo:7.0")
.WithPortBinding(27017, true)
.Build();
await _container.StartAsync();
ConnectionString = _container.GetConnectionString();
var client = new MongoClient(ConnectionString);
Database = client.GetDatabase(DatabaseName);
}
public async Task DisposeAsync()
{
if (_container != null)
{
await _container.DisposeAsync();
}
}
/// <summary>
/// æ¸
ç©ºè³æåº«ä¸çææéå - ç¨æ¼æ¸¬è©¦ééé¢
/// </summary>
public async Task ClearDatabaseAsync()
{
var collections = await Database.ListCollectionNamesAsync();
await collections.ForEachAsync(async collectionName =>
{
await Database.DropCollectionAsync(collectionName);
});
}
}
/// <summary>
/// å®ç¾©ä½¿ç¨ MongoDB Fixture çæ¸¬è©¦éå
/// </summary>
[CollectionDefinition("MongoDb Collection")]
public class MongoDbCollectionFixture : ICollectionFixture<MongoDbContainerFixture>
{
// æ¤é¡å¥ä¸éè¦å¯¦ä½ï¼å
ç¨æ¼æ¨è¨éå
}
MongoDB æä»¶æ¨¡åè¨è¨
建ç«å å«å·¢çç©ä»¶ãé£åãåå ¸çè¤éçµæ§çæä»¶æ¨¡åï¼
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace YourProject.Core.Models.Mongo;
/// <summary>
/// 使ç¨è
æä»¶ - å±ç¤º MongoDB è¤éæä»¶çµæ§
/// </summary>
public class UserDocument
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; } = string.Empty;
[BsonElement("username")]
[BsonRequired]
public string Username { get; set; } = string.Empty;
[BsonElement("email")]
[BsonRequired]
public string Email { get; set; } = string.Empty;
[BsonElement("profile")]
public UserProfile Profile { get; set; } = new();
[BsonElement("addresses")]
public List<Address> Addresses { get; set; } = new();
[BsonElement("skills")]
public List<Skill> Skills { get; set; } = new();
[BsonElement("preferences")]
public Dictionary<string, object> Preferences { get; set; } = new();
[BsonElement("created_at")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime CreatedAt { get; set; }
[BsonElement("updated_at")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime UpdatedAt { get; set; }
[BsonElement("is_active")]
public bool IsActive { get; set; } = true;
[BsonElement("version")]
public int Version { get; set; } = 1;
/// <summary>
/// æ¨è§éå®çæ¬éå¢
/// </summary>
public void IncrementVersion(DateTime updateTime)
{
Version++;
UpdatedAt = updateTime;
}
}
/// <summary>
/// 使ç¨è
æªæ¡ - å·¢çæä»¶ç¯ä¾
/// </summary>
public class UserProfile
{
[BsonElement("first_name")]
public string FirstName { get; set; } = string.Empty;
[BsonElement("last_name")]
public string LastName { get; set; } = string.Empty;
[BsonElement("birth_date")]
[BsonDateTimeOptions(Kind = DateTimeKind.Utc)]
public DateTime? BirthDate { get; set; }
[BsonElement("bio")]
public string Bio { get; set; } = string.Empty;
[BsonElement("social_links")]
public Dictionary<string, string> SocialLinks { get; set; } = new();
[BsonIgnore]
public string FullName => $"{FirstName} {LastName}".Trim();
}
/// <summary>
/// å°å模å - ç¨æ¼å°çç©ºéæ¥è©¢
/// </summary>
public class Address
{
[BsonElement("type")]
public string Type { get; set; } = string.Empty; // "home", "work", "other"
[BsonElement("city")]
public string City { get; set; } = string.Empty;
[BsonElement("country")]
public string Country { get; set; } = string.Empty;
[BsonElement("location")]
public GeoLocation? Location { get; set; }
[BsonElement("is_primary")]
public bool IsPrimary { get; set; }
}
/// <summary>
/// å°çä½ç½® - GeoJSON æ ¼å¼
/// </summary>
public class GeoLocation
{
[BsonElement("type")]
public string Type { get; set; } = "Point";
[BsonElement("coordinates")]
public double[] Coordinates { get; set; } = new double[2]; // [longitude, latitude]
public static GeoLocation CreatePoint(double longitude, double latitude)
{
return new GeoLocation
{
Type = "Point",
Coordinates = new[] { longitude, latitude }
};
}
}
/// <summary>
/// æè½æ¨¡å - é£åæ¥è©¢ç¯ä¾
/// </summary>
public class Skill
{
[BsonElement("name")]
public string Name { get; set; } = string.Empty;
[BsonElement("level")]
public SkillLevel Level { get; set; } = SkillLevel.Beginner;
[BsonElement("years_experience")]
public int YearsExperience { get; set; }
[BsonElement("verified")]
public bool Verified { get; set; }
}
/// <summary>
/// æè½çç´åè
/// </summary>
public enum SkillLevel
{
[BsonRepresentation(BsonType.String)]
Beginner,
[BsonRepresentation(BsonType.String)]
Intermediate,
[BsonRepresentation(BsonType.String)]
Advanced,
[BsonRepresentation(BsonType.String)]
Expert
}
BSON åºå忏¬è©¦
é©è BSON åºååè¡çºï¼
using MongoDB.Bson;
using AwesomeAssertions;
namespace YourProject.Integration.Tests.MongoDB;
public class MongoBsonTests
{
[Fact]
public void ObjectIdç¢ç_æç¢çææçObjectId()
{
// Arrange & Act
var objectId = ObjectId.GenerateNewId();
// Assert
objectId.Should().NotBeNull();
objectId.CreationTime.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1));
objectId.ToString().Should().HaveLength(24);
}
[Fact]
public void BsonDocument建ç«_ç¶å³å
¥nullå¼_ææ£ç¢ºèç()
{
// Arrange
var doc = new BsonDocument
{
["name"] = "John",
["email"] = BsonNull.Value,
["age"] = 25
};
// Act
var json = doc.ToJson();
// Assert
json.Should().Contain("\"email\" : null");
doc["email"].IsBsonNull.Should().BeTrue();
}
[Fact]
public void BsonArrayæä½_ç¶ä½¿ç¨è¤éé£å_ææ£ç¢ºèç()
{
// Arrange
var skills = new BsonArray
{
new BsonDocument { ["name"] = "C#", ["level"] = 5 },
new BsonDocument { ["name"] = "MongoDB", ["level"] = 3 }
};
var doc = new BsonDocument
{
["userId"] = ObjectId.GenerateNewId(),
["skills"] = skills
};
// Act
var skillsArray = doc["skills"].AsBsonArray;
var firstSkill = skillsArray[0].AsBsonDocument;
// Assert
skillsArray.Should().HaveCount(2);
firstSkill["name"].AsString.Should().Be("C#");
firstSkill["level"].AsInt32.Should().Be(5);
}
}
MongoDB CRUD 測試
using MongoDB.Driver;
using AwesomeAssertions;
using Microsoft.Extensions.Time.Testing;
namespace YourProject.Integration.Tests.MongoDB;
[Collection("MongoDb Collection")]
public class MongoUserServiceTests
{
private readonly MongoUserService _mongoUserService;
private readonly IMongoDatabase _database;
private readonly FakeTimeProvider _fakeTimeProvider;
public MongoUserServiceTests(MongoDbContainerFixture fixture)
{
_database = fixture.Database;
_fakeTimeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
// å»ºç«æå實ä¾
_mongoUserService = new MongoUserService(
_database,
Options.Create(new MongoDbSettings { UsersCollectionName = "users" }),
NullLogger<MongoUserService>.Instance,
_fakeTimeProvider);
}
[Fact]
public async Task CreateUserAsync_輸å
¥ææä½¿ç¨è
_ææå建ç«ä½¿ç¨è
()
{
// Arrange
var user = new UserDocument
{
Username = $"testuser_{Guid.NewGuid():N}",
Email = $"test_{Guid.NewGuid():N}@example.com",
Profile = new UserProfile
{
FirstName = "Test",
LastName = "User",
Bio = "Test user bio"
}
};
// Act
var result = await _mongoUserService.CreateUserAsync(user);
// Assert
result.Should().NotBeNull();
result.Username.Should().Be(user.Username);
result.Email.Should().Be(user.Email);
result.Id.Should().NotBeEmpty();
result.CreatedAt.Should().Be(_fakeTimeProvider.GetUtcNow().DateTime);
}
[Fact]
public async Task GetUserByIdAsync_輸å
¥åå¨çID_æå峿£ç¢ºä½¿ç¨è
()
{
// Arrange
var user = new UserDocument
{
Username = $"gettest_{Guid.NewGuid():N}",
Email = $"gettest_{Guid.NewGuid():N}@example.com",
Profile = new UserProfile { FirstName = "Get", LastName = "Test" }
};
var createdUser = await _mongoUserService.CreateUserAsync(user);
// Act
var result = await _mongoUserService.GetUserByIdAsync(createdUser.Id);
// Assert
result.Should().NotBeNull();
result!.Username.Should().Be(user.Username);
result.Email.Should().Be(user.Email);
}
[Fact]
public async Task UpdateUserAsync_ä½¿ç¨æ¨è§éå®_ææåæ´æ°çæ¬è()
{
// Arrange
var user = new UserDocument
{
Username = $"updatetest_{Guid.NewGuid():N}",
Email = $"updatetest_{Guid.NewGuid():N}@example.com"
};
var createdUser = await _mongoUserService.CreateUserAsync(user);
createdUser.Profile.Bio = "Updated bio";
// Act
var result = await _mongoUserService.UpdateUserAsync(createdUser);
// Assert
result.Should().NotBeNull();
result!.Version.Should().Be(2);
result.Profile.Bio.Should().Be("Updated bio");
}
[Fact]
public async Task DeleteUserAsync_輸å
¥åå¨çID_ææååªé¤ä½¿ç¨è
()
{
// Arrange
var user = new UserDocument
{
Username = $"deletetest_{Guid.NewGuid():N}",
Email = $"deletetest_{Guid.NewGuid():N}@example.com"
};
var createdUser = await _mongoUserService.CreateUserAsync(user);
// Act
var result = await _mongoUserService.DeleteUserAsync(createdUser.Id);
// Assert
result.Should().BeTrue();
var deletedUser = await _mongoUserService.GetUserByIdAsync(createdUser.Id);
deletedUser.Should().BeNull();
}
}
MongoDB ç´¢å¼æ¸¬è©¦
using MongoDB.Driver;
using AwesomeAssertions;
using System.Diagnostics;
namespace YourProject.Integration.Tests.MongoDB;
[Collection("MongoDb Collection")]
public class MongoIndexTests
{
private readonly IMongoCollection<UserDocument> _users;
private readonly ITestOutputHelper _output;
public MongoIndexTests(MongoDbContainerFixture fixture, ITestOutputHelper output)
{
_users = fixture.Database.GetCollection<UserDocument>("index_test_users");
_output = output;
}
[Fact]
public async Task CreateUniqueIndex_é»åéµä»¶å¯ä¸ç´¢å¼_æé²æ¢éè¤æå
¥()
{
// Arrange - 確ä¿éåçºç©º
await _users.DeleteManyAsync(FilterDefinition<UserDocument>.Empty);
// 建ç«å¯ä¸ç´¢å¼
var indexKeysDefinition = Builders<UserDocument>.IndexKeys.Ascending(u => u.Email);
var indexOptions = new CreateIndexOptions { Unique = true };
await _users.Indexes.CreateOneAsync(
new CreateIndexModel<UserDocument>(indexKeysDefinition, indexOptions));
var uniqueEmail = $"unique_{Guid.NewGuid():N}@example.com";
var user1 = new UserDocument { Username = "user1", Email = uniqueEmail };
var user2 = new UserDocument { Username = "user2", Email = uniqueEmail };
// Act & Assert
await _users.InsertOneAsync(user1); // ç¬¬ä¸æ¬¡æå
¥æå
var exception = await Assert.ThrowsAsync<MongoWriteException>(
() => _users.InsertOneAsync(user2));
exception.WriteError.Category.Should().Be(ServerErrorCategory.DuplicateKey);
_output.WriteLine("å¯ä¸ç´¢å¼æ¸¬è©¦éé - éè¤ç email 被æ£ç¢ºé»æ");
}
[Fact]
public async Task CompoundIndex_è¤åç´¢å¼æ¥è©¢æè½_ææåæ¥è©¢é度()
{
// Arrange - 確ä¿éåçºç©º
await _users.DeleteManyAsync(FilterDefinition<UserDocument>.Empty);
// æå
¥æ¸¬è©¦è³æ
var testUsers = Enumerable.Range(0, 1000)
.Select(i => new UserDocument
{
Username = $"user_{i:D4}",
Email = $"user{i:D4}_{Guid.NewGuid():N}@example.com",
IsActive = i % 2 == 0,
CreatedAt = DateTime.UtcNow.AddDays(-i % 365)
})
.ToList();
await _users.InsertManyAsync(testUsers);
// 建ç«è¤åç´¢å¼
var compoundIndex = Builders<UserDocument>.IndexKeys
.Ascending(u => u.IsActive)
.Descending(u => u.CreatedAt);
await _users.Indexes.CreateOneAsync(new CreateIndexModel<UserDocument>(compoundIndex));
// 測試æ¥è©¢æè½
var filter = Builders<UserDocument>.Filter.And(
Builders<UserDocument>.Filter.Eq(u => u.IsActive, true),
Builders<UserDocument>.Filter.Gte(u => u.CreatedAt, DateTime.UtcNow.AddDays(-100))
);
var stopwatch = Stopwatch.StartNew();
var results = await _users.Find(filter).ToListAsync();
stopwatch.Stop();
_output.WriteLine($"æ¥è©¢æé: {stopwatch.ElapsedMilliseconds}ms, çµææ¸é: {results.Count}");
// Assert
results.Should().NotBeEmpty();
}
}
Redis 容å¨å測試
Redis Container Fixture
using StackExchange.Redis;
using Testcontainers.Redis;
namespace YourProject.Integration.Tests.Fixtures;
/// <summary>
/// Redis å®¹å¨ Fixture - 管ç Redis 容å¨çå½é±æ
/// </summary>
public class RedisContainerFixture : IAsyncLifetime
{
private RedisContainer? _container;
public IConnectionMultiplexer Connection { get; private set; } = null!;
public IDatabase Database { get; private set; } = null!;
public string ConnectionString { get; private set; } = string.Empty;
public async Task InitializeAsync()
{
// ä½¿ç¨ Redis 7.2 çæ¬
_container = new RedisBuilder()
.WithImage("redis:7.2")
.WithPortBinding(6379, true)
.Build();
await _container.StartAsync();
ConnectionString = _container.GetConnectionString();
Connection = await ConnectionMultiplexer.ConnectAsync(ConnectionString);
Database = Connection.GetDatabase();
}
public async Task DisposeAsync()
{
if (Connection != null)
{
await Connection.DisposeAsync();
}
if (_container != null)
{
await _container.DisposeAsync();
}
}
/// <summary>
/// æ¸
ç©ºè³æåº« - ä½¿ç¨ KeyDelete èé FLUSHDBï¼é¿å
æ¬éåé¡ï¼
/// </summary>
public async Task ClearDatabaseAsync()
{
var server = Connection.GetServer(Connection.GetEndPoints().First());
var keys = server.Keys(Database.Database);
if (keys.Any())
{
await Database.KeyDeleteAsync(keys.ToArray());
}
}
}
[CollectionDefinition("Redis Collection")]
public class RedisCollectionFixture : ICollectionFixture<RedisContainerFixture>
{
}
Redis å¿«åæ¨¡å
using System.Text.Json.Serialization;
namespace YourProject.Core.Models.Redis;
/// <summary>
/// éç¨å¿«åå
è£å¨ - æä¾è±å¯çå¿«åå
è³æ
/// </summary>
public class CacheItem<T>
{
[JsonPropertyName("data")]
public T Data { get; set; } = default!;
[JsonPropertyName("created_at")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("expires_at")]
public DateTime? ExpiresAt { get; set; }
[JsonPropertyName("key")]
public string Key { get; set; } = string.Empty;
[JsonPropertyName("tags")]
public List<string> Tags { get; set; } = new();
[JsonPropertyName("access_count")]
public int AccessCount { get; set; }
[JsonPropertyName("version")]
public int Version { get; set; } = 1;
[JsonIgnore]
public bool IsExpired => ExpiresAt.HasValue && DateTime.UtcNow > ExpiresAt.Value;
[JsonIgnore]
public double TtlSeconds => ExpiresAt.HasValue
? Math.Max(0, ExpiresAt.Value.Subtract(DateTime.UtcNow).TotalSeconds)
: -1;
public static CacheItem<T> Create(string key, T data, TimeSpan? ttl = null, params string[] tags)
{
return new CacheItem<T>
{
Key = key,
Data = data,
CreatedAt = DateTime.UtcNow,
ExpiresAt = ttl.HasValue ? DateTime.UtcNow.Add(ttl.Value) : null,
Tags = tags.ToList()
};
}
}
/// <summary>
/// 使ç¨è
Session - Hash çµæ§ç¯ä¾
/// </summary>
public class UserSession
{
public string UserId { get; set; } = string.Empty;
public string SessionId { get; set; } = string.Empty;
public string IpAddress { get; set; } = string.Empty;
public string UserAgent { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
/// <summary>
/// æè¿ç覽ç´é - List çµæ§ç¯ä¾
/// </summary>
public class RecentView
{
public string ItemId { get; set; } = string.Empty;
public string ItemType { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
}
/// <summary>
/// æè¡æ¦é
ç® - Sorted Set çµæ§ç¯ä¾
/// </summary>
public class LeaderboardEntry
{
public string UserId { get; set; } = string.Empty;
public string Username { get; set; } = string.Empty;
public double Score { get; set; }
}
Redis äºç¨®è³æçµæ§æ¸¬è©¦
using StackExchange.Redis;
using AwesomeAssertions;
namespace YourProject.Integration.Tests.Redis;
[Collection("Redis Collection")]
public class RedisCacheServiceTests
{
private readonly RedisCacheService _redisCacheService;
private readonly RedisContainerFixture _fixture;
public RedisCacheServiceTests(RedisContainerFixture fixture)
{
_fixture = fixture;
_redisCacheService = new RedisCacheService(
fixture.Connection,
Options.Create(new RedisSettings()),
NullLogger<RedisCacheService>.Instance,
TimeProvider.System);
}
#region String 測試
[Fact]
public async Task SetStringAsync_輸å
¥å串å¼_ææåè¨å®å¿«å()
{
// Arrange
var key = $"test_string_{Guid.NewGuid():N}";
var value = "test_string_value";
// Act
var result = await _redisCacheService.SetStringAsync(key, value);
// Assert
result.Should().BeTrue();
var retrieved = await _redisCacheService.GetStringAsync<string>(key);
retrieved.Should().Be(value);
}
[Fact]
public async Task SetObjectCacheAsync_輸å
¥ç©ä»¶_ææååºåå並快å()
{
// Arrange
var key = $"object_test_{Guid.NewGuid():N}";
var user = new UserDocument
{
Username = "objecttest",
Email = "object@test.com",
Profile = new UserProfile { FirstName = "Object", LastName = "Test" }
};
// Act
var result = await _redisCacheService.SetStringAsync(key, user, TimeSpan.FromMinutes(30));
// Assert
result.Should().BeTrue();
var retrieved = await _redisCacheService.GetStringAsync<UserDocument>(key);
retrieved.Should().NotBeNull();
retrieved!.Username.Should().Be("objecttest");
}
[Fact]
public async Task SetMultipleStringAsync_輸å
¥å¤åéµå¼å°_ææåæ¹æ¬¡è¨å®()
{
// Arrange
var prefix = Guid.NewGuid().ToString("N")[..8];
var keyValues = new Dictionary<string, string>
{
{ $"multi1_{prefix}", "value1" },
{ $"multi2_{prefix}", "value2" },
{ $"multi3_{prefix}", "value3" }
};
// Act
var result = await _redisCacheService.SetMultipleStringAsync(keyValues);
// Assert
result.Should().BeTrue();
foreach (var kvp in keyValues)
{
var value = await _redisCacheService.GetStringAsync<string>(kvp.Key);
value.Should().Be(kvp.Value);
}
}
#endregion
#region Hash 測試
[Fact]
public async Task SetHashAsync_輸å
¥å串å¼_æè¨å®Hashæ¬ä½()
{
// Arrange
var key = $"hash_test_{Guid.NewGuid():N}";
var field = "test_field";
var value = "test_value";
// Act
var result = await _redisCacheService.SetHashAsync(key, field, value, TimeSpan.FromMinutes(30));
// Assert
result.Should().BeTrue();
var retrieved = await _redisCacheService.GetHashAsync<string>(key, field);
retrieved.Should().Be(value);
}
[Fact]
public async Task SetHashAllAsync_輸å
¥ç©ä»¶_æè¨å®å®æ´Hash()
{
// Arrange
var key = $"hash_all_{Guid.NewGuid():N}";
var session = new UserSession
{
UserId = "user123",
SessionId = "session456",
IpAddress = "192.168.1.1",
UserAgent = "Test Browser",
IsActive = true
};
// Act
var result = await _redisCacheService.SetHashAllAsync(key, session, TimeSpan.FromHours(1));
// Assert
result.Should().BeTrue();
var retrieved = await _redisCacheService.GetHashAllAsync<UserSession>(key);
retrieved.Should().NotBeNull();
retrieved!.UserId.Should().Be("user123");
retrieved.SessionId.Should().Be("session456");
}
#endregion
#region List 測試
[Fact]
public async Task ListLeftPushAsync_輸å
¥å¼_ææ°å¢å°Listå·¦å´()
{
// Arrange
var key = $"list_test_{Guid.NewGuid():N}";
var view1 = new RecentView { ItemId = "item1", ItemType = "product", Title = "Product 1" };
var view2 = new RecentView { ItemId = "item2", ItemType = "product", Title = "Product 2" };
// Act
var count1 = await _redisCacheService.ListLeftPushAsync(key, view1);
var count2 = await _redisCacheService.ListLeftPushAsync(key, view2);
// Assert
count1.Should().Be(1);
count2.Should().Be(2);
var views = await _redisCacheService.ListRangeAsync<RecentView>(key);
views.Should().HaveCount(2);
views[0].ItemId.Should().Be("item2"); // æå¾å å
¥ç卿åé¢
views[1].ItemId.Should().Be("item1");
}
#endregion
#region Set 測試
[Fact]
public async Task SetAddAsync_輸å
¥å¼_ææ°å¢å°Set()
{
// Arrange
var key = $"set_test_{Guid.NewGuid():N}";
var tag1 = "programming";
var tag2 = "testing";
var tag3 = "programming"; // éè¤
// Act
var result1 = await _redisCacheService.SetAddAsync(key, tag1);
var result2 = await _redisCacheService.SetAddAsync(key, tag2);
var result3 = await _redisCacheService.SetAddAsync(key, tag3);
// Assert
result1.Should().BeTrue();
result2.Should().BeTrue();
result3.Should().BeFalse(); // éè¤é
ç®åå³ false
var tags = await _redisCacheService.SetMembersAsync<string>(key);
tags.Should().HaveCount(2);
tags.Should().Contain("programming");
tags.Should().Contain("testing");
}
#endregion
#region Sorted Set 測試
[Fact]
public async Task SortedSetAddAsync_輸å
¥åæ¸åæå¡_ææåæ°å¢å°æåºéå()
{
// Arrange
var key = $"sorted_set_{Guid.NewGuid():N}";
var entry1 = new LeaderboardEntry { UserId = "user1", Username = "Player1", Score = 100 };
var entry2 = new LeaderboardEntry { UserId = "user2", Username = "Player2", Score = 200 };
// Act
var result1 = await _redisCacheService.SortedSetAddAsync(key, entry1, entry1.Score);
var result2 = await _redisCacheService.SortedSetAddAsync(key, entry2, entry2.Score);
// Assert
result1.Should().BeTrue();
result2.Should().BeTrue();
var rankings = await _redisCacheService.SortedSetRangeWithScoresAsync<LeaderboardEntry>(
key, 0, -1, Order.Descending);
rankings.Should().HaveCount(2);
rankings[0].Member.Username.Should().Be("Player2"); // 忏é«çå¨åé¢
rankings[0].Score.Should().Be(200);
}
#endregion
#region TTL èéææ¸¬è©¦
[Fact]
public async Task ExpireAsync_輸å
¥éææé_ææ£ç¢ºè¨å®TTL()
{
// Arrange
var key = $"expire_test_{Guid.NewGuid():N}";
await _redisCacheService.SetStringAsync(key, "expire_value");
// Act
var result = await _redisCacheService.ExpireAsync(key, TimeSpan.FromMinutes(5));
// Assert
result.Should().BeTrue();
var ttl = await _redisCacheService.GetTtlAsync(key);
ttl.Should().NotBeNull();
ttl!.Value.TotalMinutes.Should().BeGreaterThan(4);
}
#endregion
#region è³æé颿¸¬è©¦
[Fact]
public async Task æ¸¬è©¦è³æéé¢_å¤å測試åæå·è¡_æä¸äºç¸å½±é¿()
{
// Arrange
var testId = Guid.NewGuid().ToString("N")[..8];
var key1 = $"isolation_test:{testId}:key1";
var key2 = $"isolation_test:{testId}:key2";
// Act
await _redisCacheService.SetStringAsync(key1, "value1");
await _redisCacheService.SetStringAsync(key2, "value2");
// Assert
var value1 = await _redisCacheService.GetStringAsync<string>(key1);
var value2 = await _redisCacheService.GetStringAsync<string>(key2);
value1.Should().Be("value1");
value2.Should().Be("value2");
// Cleanup
await _redisCacheService.DeleteAsync(key1);
await _redisCacheService.DeleteAsync(key2);
}
#endregion
}
æä½³å¯¦è¸
1. Collection Fixture 模å¼
ä½¿ç¨ Collection Fixture å ±äº«å®¹å¨ï¼é¿å æ¯å測試éå容å¨ï¼
// å®ç¾©éå
[CollectionDefinition("MongoDb Collection")]
public class MongoDbCollectionFixture : ICollectionFixture<MongoDbContainerFixture> { }
// 使ç¨éå
[Collection("MongoDb Collection")]
public class MyMongoTests
{
public MyMongoTests(MongoDbContainerFixture fixture)
{
// 使ç¨å
±äº«ç容å¨
}
}
2. è³æéé¢çç¥
ç¢ºä¿æ¸¬è©¦éä¸äºç¸å¹²æ¾ï¼
// MongoDBï¼ä½¿ç¨å¯ä¸ç Email/Username
var user = new UserDocument
{
Username = $"testuser_{Guid.NewGuid():N}",
Email = $"test_{Guid.NewGuid():N}@example.com"
};
// Redisï¼ä½¿ç¨å¯ä¸ç Key åç¶´
var testId = Guid.NewGuid().ToString("N")[..8];
var key = $"test:{testId}:mykey";
3. æ¸ ççç¥
// MongoDBï¼æ¸¬è©¦å¾æ¸
ç
await fixture.ClearDatabaseAsync();
// Redisï¼ä½¿ç¨ KeyDelete èé FLUSHDBï¼é¿å
æ¬éåé¡ï¼
var keys = server.Keys(database.Database);
if (keys.Any())
{
await database.KeyDeleteAsync(keys.ToArray());
}
4. æè½èé
| çç¥ | 說æ |
|---|---|
| Collection Fixture | 容å¨åªåå䏿¬¡ï¼ç¯ç 80%+ æé |
| è³æéé¢ | 使ç¨å¯ä¸ Key/ID è鿏 ç©ºè³æåº« |
| æ¹æ¬¡æä½ | ä½¿ç¨ InsertManyAsyncãSetMultipleStringAsync |
| ç´¢å¼å»ºç« | å¨ Fixture åå§åæå»ºç«ç´¢å¼ |
常è¦åé¡
Redis FLUSHDB æ¬éåé¡
æäº Redis 容卿 åæªé è¨ä¸åç¨ admin 模å¼ï¼
// â é¯èª¤ï¼å¯è½å¤±æ
await server.FlushDatabaseAsync();
// â
æ£ç¢ºï¼ä½¿ç¨ KeyDelete
var keys = server.Keys(database.Database);
if (keys.Any())
{
await database.KeyDeleteAsync(keys.ToArray());
}
MongoDB å¯ä¸ç´¢å¼éè¤æå ¥
// 測試æä½¿ç¨å¯ä¸ç Email é¿å
è¡çª
var uniqueEmail = $"test_{Guid.NewGuid():N}@example.com";
容å¨ååè¶ æ
// å¢å çå¾
æé
_container = new MongoDbBuilder()
.WithImage("mongo:7.0")
.WithWaitStrategy(Wait.ForUnixContainer()
.UntilPortIsAvailable(27017))
.Build();
ç¸éæè½
- testcontainers-database – PostgreSQL/MSSQL 容å¨å測試
- aspnet-integration-testing – ASP.NET Core æ´å測試
- nsubstitute-mocking – 測試æ¿èº«è Mock
åèè³æº
åå§æç«
æ¬æè½å §å®¹æç èªãèæ´¾è»é«å·¥ç¨å¸«ç測試修練 – 30 å¤©ææ°ãç³»åæç« ï¼
- Day 22 – Testcontainers æ´å測試ï¼MongoDB å Redis åºç¤å°é²é
- éµäººè³½æç« ï¼https://ithelp.ithome.com.tw/articles/10376740
- ç¯ä¾ç¨å¼ç¢¼ï¼https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day22