A:common 文件测试

This commit is contained in:
jimmy committed 2025-11-13 18:51:31 +08:00
1 parent 9cfbc3e458
commit fdf9a3c762
7 files changed
+2610

No files matched your search

+204
View File
@@ -0,0 +1,204 @@
using Common.ExpandException;
namespace Common.Test;
/// <summary>
/// ExceptionExpand 自定义异常测试
/// 展示带错误码和建议的异常处理
/// </summary>
[TestClass]
public class ExceptionExpandTests
{
/// <summary>
/// 测试:创建带错误码和建议的异常
/// </summary>
[TestMethod]
public void Constructor_CreatesExceptionWithCodeAndSuggestion()
{
// Arrange & Act
var exception = new ExceptionExpand(1001L, "Process failed", "Retry with admin rights");
// Assert
Assert.AreEqual(1001L, exception.ErrorCode);
Assert.AreEqual("Process failed", exception.Message);
Assert.AreEqual("Retry with admin rights", exception.Suggestion);
}
/// <summary>
/// 测试:创建带内部异常的自定义异常
/// </summary>
[TestMethod]
public void Constructor_WithInnerException_PreservesInnerException()
{
// Arrange
var innerException = new InvalidOperationException("Inner error");
// Act
var exception = new ExceptionExpand(2001L, "Outer error", "Check configuration", innerException);
// Assert
Assert.AreEqual(2001L, exception.ErrorCode);
Assert.AreEqual("Outer error", exception.Message);
Assert.AreEqual("Check configuration", exception.Suggestion);
Assert.AreSame(innerException, exception.InnerException);
}
/// <summary>
/// 测试:ToString 包含所有信息
/// </summary>
[TestMethod]
public void ToString_IncludesAllInformation()
{
// Arrange
var exception = new ExceptionExpand(404L, "Resource not found", "Check the resource ID");
// Act
var message = exception.ToString();
// Assert
Assert.IsTrue(message.Contains("404"));
Assert.IsTrue(message.Contains("Resource not found"));
Assert.IsTrue(message.Contains("Check the resource ID"));
}
/// <summary>
/// 测试:抛出和捕获自定义异常
/// </summary>
[TestMethod]
public void ThrowAndCatch_WorksCorrectly()
{
// Act & Assert
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
throw new ExceptionExpand(500L, "Server error", "Contact support");
});
Assert.AreEqual(500L, exception.ErrorCode);
Assert.AreEqual("Server error", exception.Message);
Assert.AreEqual("Contact support", exception.Suggestion);
}
/// <summary>
/// 场景测试:使用预定义的异常枚举
/// 演示如何使用 ExampleExceptionEnum 中的预定义异常
/// </summary>
[TestMethod]
public void Scenario_UsePredefinedExceptions()
{
// Act: 抛出预定义异常
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
throw ExampleExceptionEnum.ProcessGetException;
});
// Assert: 验证预定义异常的属性
Assert.AreEqual(100001L, exception.ErrorCode);
Assert.IsTrue(exception.Message.Contains("进程消息获取失败"));
Assert.IsTrue(exception.Suggestion.Contains("管理员权限"));
}
/// <summary>
/// 场景测试:在业务逻辑中使用自定义异常
/// </summary>
[TestMethod]
public void Scenario_UseInBusinessLogic()
{
// Act: 执行可能失败的业务操作
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
ProcessBusinessOperation(-1);
});
// Assert
Assert.AreEqual(4001L, exception.ErrorCode);
Assert.AreEqual("Invalid user ID", exception.Message);
Assert.AreEqual("User ID must be positive", exception.Suggestion);
}
/// <summary>
/// 场景测试:异常链
/// 演示如何使用 InnerException 构建异常链
/// </summary>
[TestMethod]
public void Scenario_ExceptionChain()
{
// Arrange: 创建异常链
var databaseException = new InvalidOperationException("Database connection failed");
var serviceException = new ExceptionExpand(
5001L,
"Service unavailable",
"Check database connection",
databaseException
);
// Act: 捕获并检查异常链
var exception = Assert.ThrowsException<ExceptionExpand>(() =>
{
throw serviceException;
});
// Assert: 验证异常链
Assert.AreEqual(5001L, exception.ErrorCode);
Assert.IsNotNull(exception.InnerException);
Assert.IsInstanceOfType(exception.InnerException, typeof(InvalidOperationException));
Assert.AreEqual("Database connection failed", exception.InnerException.Message);
}
/// <summary>
/// 场景测试:根据错误码进行不同处理
/// </summary>
[TestMethod]
public void Scenario_HandleByErrorCode()
{
// Arrange: 模拟不同的错误场景
var errors = new[]
{
new ExceptionExpand(400L, "Bad request", "Check input parameters"),
new ExceptionExpand(404L, "Not found", "Verify resource ID"),
new ExceptionExpand(500L, "Internal error", "Contact support")
};
// Act & Assert: 根据错误码分类处理
foreach (var error in errors)
{
var handled = HandleError(error);
if (error.ErrorCode == 400L)
Assert.AreEqual("Client error", handled);
else if (error.ErrorCode == 404L)
Assert.AreEqual("Client error", handled);
else if (error.ErrorCode == 500L)
Assert.AreEqual("Server error", handled);
}
}
#region 测试辅助方法
/// <summary>
/// 模拟业务操作
/// </summary>
private void ProcessBusinessOperation(int userId)
{
if (userId <= 0)
{
throw new ExceptionExpand(4001L, "Invalid user ID", "User ID must be positive");
}
// 正常处理逻辑...
}
/// <summary>
/// 根据错误码处理错误
/// </summary>
private string HandleError(ExceptionExpand exception)
{
return exception.ErrorCode switch
{
>= 400L and < 500L => "Client error",
>= 500L and < 600L => "Server error",
_ => "Unknown error"
};
}
#endregion
}
+510
View File
@@ -0,0 +1,510 @@
using Common.Utils;
using System.Net;
namespace Common.Test;
/// <summary>
/// JsonHelper 测试
/// 展示 JSON 序列化和反序列化功能
/// </summary>
[TestClass]
public class JsonHelperTests
{
#region 基础序列化测试
/// <summary>
/// 测试:序列化简单对象
/// </summary>
[TestMethod]
public void ToJson_SimpleObject_SerializesCorrectly()
{
// Arrange
var person = new Person
{
Name = "John Doe",
Age = 30,
Email = "john@example.com"
};
// Act
var json = JsonHelper.ToJson(person);
// Assert
Assert.IsTrue(json.Contains("\"name\": \"John Doe\"")); // camelCase
Assert.IsTrue(json.Contains("\"age\": 30"));
Assert.IsTrue(json.Contains("\"email\": \"john@example.com\""));
}
/// <summary>
/// 测试:序列化使用 camelCase 命名策略
/// </summary>
[TestMethod]
public void ToJson_UsesCamelCase()
{
// Arrange
var config = new AppConfig
{
AppName = "MyApp",
MaxConnections = 100
};
// Act
var json = JsonHelper.ToJson(config);
// Assert: 属性名应该是 camelCase
Assert.IsTrue(json.Contains("\"appName\""));
Assert.IsTrue(json.Contains("\"maxConnections\""));
Assert.IsFalse(json.Contains("\"AppName\"")); // 不应该是 PascalCase
}
/// <summary>
/// 测试:序列化带格式化(缩进)
/// </summary>
[TestMethod]
public void ToJson_WritesIndented()
{
// Arrange
var person = new Person { Name = "Jane", Age = 25 };
// Act
var json = JsonHelper.ToJson(person);
// Assert: 应该包含换行符(表示有缩进)
Assert.IsTrue(json.Contains("\n") || json.Contains("\r\n"));
}
#endregion
#region 基础反序列化测试
/// <summary>
/// 测试:反序列化简单对象
/// </summary>
[TestMethod]
public void FromJson_ValidJson_DeserializesCorrectly()
{
// Arrange
var json = """
{
"name": "Alice",
"age": 28,
"email": "alice@example.com"
}
""";
// Act
var person = JsonHelper.FromJson<Person>(json);
// Assert
Assert.IsNotNull(person);
Assert.AreEqual("Alice", person.Name);
Assert.AreEqual(28, person.Age);
Assert.AreEqual("alice@example.com", person.Email);
}
/// <summary>
/// 测试:反序列化 camelCase JSON
/// </summary>
[TestMethod]
public void FromJson_CamelCaseJson_DeserializesCorrectly()
{
// Arrange: JSON 使用 camelCase
var json = """
{
"appName": "TestApp",
"maxConnections": 50
}
""";
// Act
var config = JsonHelper.FromJson<AppConfig>(json);
// Assert
Assert.IsNotNull(config);
Assert.AreEqual("TestApp", config.AppName);
Assert.AreEqual(50, config.MaxConnections);
}
#endregion
#region 特殊类型处理测试
/// <summary>
/// 测试:序列化和反序列化枚举(使用字符串)
/// </summary>
[TestMethod]
public void ToJson_Enum_SerializesAsString()
{
// Arrange
var data = new DataWithEnum
{
Status = Status.Active,
Priority = Priority.High
};
// Act
var json = JsonHelper.ToJson(data);
// Assert: 枚举应该序列化为字符串(camelCase)
Assert.IsTrue(json.Contains("\"active\""));
Assert.IsTrue(json.Contains("\"high\""));
Assert.IsFalse(json.Contains("0")); // 不应该是数字
Assert.IsFalse(json.Contains("1"));
}
/// <summary>
/// 测试:反序列化枚举字符串
/// </summary>
[TestMethod]
public void FromJson_EnumString_DeserializesCorrectly()
{
// Arrange
var json = """
{
"status": "active",
"priority": "low"
}
""";
// Act
var data = JsonHelper.FromJson<DataWithEnum>(json);
// Assert
Assert.IsNotNull(data);
Assert.AreEqual(Status.Active, data.Status);
Assert.AreEqual(Priority.Low, data.Priority);
}
/// <summary>
/// 测试:序列化 IPAddress
/// </summary>
[TestMethod]
public void ToJson_IPAddress_SerializesCorrectly()
{
// Arrange
var networkInfo = new NetworkInfo
{
ServerIp = IPAddress.Parse("192.168.1.1"),
ClientIp = IPAddress.Parse("10.0.0.5")
};
// Act
var json = JsonHelper.ToJson(networkInfo);
// Assert
Assert.IsTrue(json.Contains("\"192.168.1.1\""));
Assert.IsTrue(json.Contains("\"10.0.0.5\""));
}
/// <summary>
/// 测试:反序列化 IPAddress
/// </summary>
[TestMethod]
public void FromJson_IPAddress_DeserializesCorrectly()
{
// Arrange
var json = """
{
"serverIp": "192.168.1.1",
"clientIp": "10.0.0.5"
}
""";
// Act
var networkInfo = JsonHelper.FromJson<NetworkInfo>(json);
// Assert
Assert.IsNotNull(networkInfo);
Assert.AreEqual(IPAddress.Parse("192.168.1.1"), networkInfo.ServerIp);
Assert.AreEqual(IPAddress.Parse("10.0.0.5"), networkInfo.ClientIp);
}
/// <summary>
/// 测试:IPv6 地址序列化和反序列化
/// </summary>
[TestMethod]
public void ToJson_IPv6Address_SerializesCorrectly()
{
// Arrange
var networkInfo = new NetworkInfo
{
ServerIp = IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
ClientIp = IPAddress.IPv6Loopback
};
// Act
var json = JsonHelper.ToJson(networkInfo);
var deserialized = JsonHelper.FromJson<NetworkInfo>(json);
// Assert
Assert.IsNotNull(deserialized);
Assert.AreEqual(networkInfo.ServerIp, deserialized.ServerIp);
Assert.AreEqual(IPAddress.IPv6Loopback, deserialized.ClientIp);
}
#endregion
#region 复杂对象测试
/// <summary>
/// 测试:序列化嵌套对象
/// </summary>
[TestMethod]
public void ToJson_NestedObject_SerializesCorrectly()
{
// Arrange
var order = new Order
{
OrderId = "ORD-001",
Customer = new Person
{
Name = "Bob",
Age = 35,
Email = "bob@example.com"
},
Items = new List<OrderItem>
{
new() { ProductName = "Laptop", Price = 999.99, Quantity = 1 },
new() { ProductName = "Mouse", Price = 29.99, Quantity = 2 }
}
};
// Act
var json = JsonHelper.ToJson(order);
var deserialized = JsonHelper.FromJson<Order>(json);
// Assert
Assert.IsNotNull(deserialized);
Assert.AreEqual("ORD-001", deserialized.OrderId);
Assert.AreEqual("Bob", deserialized.Customer?.Name);
Assert.AreEqual(2, deserialized.Items?.Count);
Assert.AreEqual("Laptop", deserialized.Items?[0].ProductName);
Assert.AreEqual(999.99, deserialized.Items?[0].Price);
}
/// <summary>
/// 测试:序列化集合
/// </summary>
[TestMethod]
public void ToJson_Collection_SerializesCorrectly()
{
// Arrange
var people = new List<Person>
{
new() { Name = "Alice", Age = 30 },
new() { Name = "Bob", Age = 35 },
new() { Name = "Charlie", Age = 28 }
};
// Act
var json = JsonHelper.ToJson(people);
var deserialized = JsonHelper.FromJson<List<Person>>(json);
// Assert
Assert.IsNotNull(deserialized);
Assert.AreEqual(3, deserialized.Count);
Assert.AreEqual("Alice", deserialized[0].Name);
Assert.AreEqual("Bob", deserialized[1].Name);
}
#endregion
#region 实际应用场景测试
/// <summary>
/// 场景测试:配置文件序列化
/// 演示如何使用 JsonHelper 读写配置文件
/// </summary>
[TestMethod]
public void Scenario_SaveAndLoadConfiguration()
{
// Arrange: 创建配置对象
var originalConfig = new AppConfig
{
AppName = "MyApplication",
MaxConnections = 100,
Settings = new Dictionary<string, string>
{
["Theme"] = "Dark",
["Language"] = "en-US"
}
};
// Act: 序列化配置
var json = JsonHelper.ToJson(originalConfig);
// 模拟保存到文件后再读取
var loadedConfig = JsonHelper.FromJson<AppConfig>(json);
// Assert: 验证配置正确加载
Assert.IsNotNull(loadedConfig);
Assert.AreEqual(originalConfig.AppName, loadedConfig.AppName);
Assert.AreEqual(originalConfig.MaxConnections, loadedConfig.MaxConnections);
Assert.AreEqual("Dark", loadedConfig.Settings?["Theme"]);
Assert.AreEqual("en-US", loadedConfig.Settings?["Language"]);
}
/// <summary>
/// 场景测试:API 请求和响应
/// 演示如何序列化 API 数据
/// </summary>
[TestMethod]
public void Scenario_ApiRequestResponse()
{
// Arrange: 创建 API 请求
var request = new ApiRequest
{
Endpoint = "/api/users",
Method = "POST",
Headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json",
["Authorization"] = "Bearer token123"
},
Body = new Person
{
Name = "New User",
Age = 25,
Email = "newuser@example.com"
}
};
// Act: 序列化请求
var requestJson = JsonHelper.ToJson(request);
// 模拟 API 调用后收到响应
var responseJson = """
{
"success": true,
"data": {
"name": "New User",
"age": 25,
"email": "newuser@example.com"
},
"message": "User created successfully"
}
""";
var response = JsonHelper.FromJson<ApiResponse<Person>>(responseJson);
// Assert
Assert.IsNotNull(response);
Assert.IsTrue(response.Success);
Assert.AreEqual("User created successfully", response.Message);
Assert.IsNotNull(response.Data);
Assert.AreEqual("New User", response.Data.Name);
}
/// <summary>
/// 场景测试:网络配置持久化
/// 演示如何序列化包含 IPAddress 的复杂对象
/// </summary>
[TestMethod]
public void Scenario_NetworkConfigurationPersistence()
{
// Arrange: 创建网络配置
var networkConfig = new NetworkConfig
{
ServerIp = IPAddress.Parse("192.168.1.100"),
DnsServers = new List<IPAddress>
{
IPAddress.Parse("8.8.8.8"),
IPAddress.Parse("8.8.4.4")
},
Gateway = IPAddress.Parse("192.168.1.1")
};
// Act: 序列化并反序列化
var json = JsonHelper.ToJson(networkConfig);
var loaded = JsonHelper.FromJson<NetworkConfig>(json);
// Assert
Assert.IsNotNull(loaded);
Assert.AreEqual(networkConfig.ServerIp, loaded.ServerIp);
Assert.AreEqual(2, loaded.DnsServers?.Count);
Assert.AreEqual(IPAddress.Parse("8.8.8.8"), loaded.DnsServers?[0]);
Assert.AreEqual(networkConfig.Gateway, loaded.Gateway);
}
#endregion
#region 测试辅助类型
private class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public string? Email { get; set; }
}
private class AppConfig
{
public string AppName { get; set; } = "";
public int MaxConnections { get; set; }
public Dictionary<string, string>? Settings { get; set; }
}
private enum Status
{
Inactive = 0,
Active = 1,
Suspended = 2
}
private enum Priority
{
Low = 0,
Medium = 1,
High = 2
}
private class DataWithEnum
{
public Status Status { get; set; }
public Priority Priority { get; set; }
}
private class NetworkInfo
{
public IPAddress? ServerIp { get; set; }
public IPAddress? ClientIp { get; set; }
}
private class Order
{
public string OrderId { get; set; } = "";
public Person? Customer { get; set; }
public List<OrderItem>? Items { get; set; }
}
private class OrderItem
{
public string ProductName { get; set; } = "";
public double Price { get; set; }
public int Quantity { get; set; }
}
private class ApiRequest
{
public string Endpoint { get; set; } = "";
public string Method { get; set; } = "";
public Dictionary<string, string>? Headers { get; set; }
public object? Body { get; set; }
}
private class ApiResponse<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
public string Message { get; set; } = "";
}
private class NetworkConfig
{
public IPAddress? ServerIp { get; set; }
public List<IPAddress>? DnsServers { get; set; }
public IPAddress? Gateway { get; set; }
}
#endregion
}
+880
View File
@@ -0,0 +1,880 @@
using Common.Results;
using Common.ExpandException;
namespace Common.Test;
/// <summary>
/// Result 模式测试
/// 展示函数式错误处理的所有功能
/// </summary>
[TestClass]
public class ResultTests
{
#region 基础创建和状态测试
/// <summary>
/// 测试:创建成功的 Result
/// </summary>
[TestMethod]
public void Success_CreatesSuccessResult()
{
// Act: 创建成功的无返回值 Result
var result = Result.Success();
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.IsFalse(result.IsFailure);
Assert.IsNull(result.Exception);
Assert.IsNull(result.ErrorMessage);
Assert.IsNull(result.ErrorCode);
}
/// <summary>
/// 测试:创建成功的 Result<T>
/// </summary>
[TestMethod]
public void Success_WithValue_CreatesSuccessResultWithValue()
{
// Act: 创建带值的成功 Result
var result = Result.Success(42);
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual(42, result.Value);
}
/// <summary>
/// 测试:创建失败的 Result(使用错误消息)
/// </summary>
[TestMethod]
public void Failure_WithMessage_CreatesFailureResult()
{
// Act
var result = Result.Failure<int>("Something went wrong");
// Assert
Assert.IsFalse(result.IsSuccess);
Assert.IsTrue(result.IsFailure);
Assert.AreEqual("Something went wrong", result.ErrorMessage);
Assert.IsNull(result.ErrorCode);
}
/// <summary>
/// 测试:创建失败的 Result(使用异常)
/// </summary>
[TestMethod]
public void Failure_WithException_CreatesFailureResult()
{
// Arrange
var exception = new InvalidOperationException("Invalid operation");
// Act
var result = Result.Failure<int>(exception);
// Assert
Assert.IsTrue(result.IsFailure);
Assert.AreSame(exception, result.Exception);
Assert.AreEqual("Invalid operation", result.ErrorMessage);
}
/// <summary>
/// 测试:创建带错误码的失败 Result
/// </summary>
[TestMethod]
public void Failure_WithErrorCode_CreatesFailureResultWithCode()
{
// Act
var result = Result.Failure<int>(404L, "Not found");
// Assert
Assert.IsTrue(result.IsFailure);
Assert.AreEqual(404L, result.ErrorCode);
Assert.AreEqual("Not found", result.ErrorMessage);
}
/// <summary>
/// 测试:从 ExceptionExpand 创建失败 Result
/// </summary>
[TestMethod]
public void Failure_WithExceptionExpand_ExtractsErrorCode()
{
// Arrange
var exception = new ExceptionExpand(1001L, "Custom error", "Try again");
// Act
var result = Result.Failure<int>(exception);
// Assert
Assert.IsTrue(result.IsFailure);
Assert.AreEqual(1001L, result.ErrorCode);
Assert.AreEqual("Custom error", result.ErrorMessage);
Assert.AreSame(exception, result.Exception);
}
#endregion
#region Try 模式测试
/// <summary>
/// 测试:Try 捕获成功的操作
/// </summary>
[TestMethod]
public void Try_SuccessfulAction_ReturnsSuccess()
{
// Arrange
var executed = false;
// Act
var result = Result.Try(() =>
{
executed = true;
});
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.IsTrue(executed);
}
/// <summary>
/// 测试:Try 捕获失败的操作
/// </summary>
[TestMethod]
public void Try_FailingAction_ReturnsFailure()
{
// Act
var result = Result.Try(() =>
{
throw new InvalidOperationException("Operation failed");
});
// Assert
Assert.IsTrue(result.IsFailure);
Assert.IsInstanceOfType(result.Exception, typeof(InvalidOperationException));
Assert.AreEqual("Operation failed", result.ErrorMessage);
}
/// <summary>
/// 测试:Try 捕获返回值的成功操作
/// </summary>
[TestMethod]
public void Try_SuccessfulFunc_ReturnsSuccessWithValue()
{
// Act
var result = Result.Try(() => int.Parse("123"));
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual(123, result.Value);
}
/// <summary>
/// 测试:Try 捕获返回值的失败操作
/// </summary>
[TestMethod]
public void Try_FailingFunc_ReturnsFailure()
{
// Act
var result = Result.Try(() => int.Parse("invalid"));
// Assert
Assert.IsTrue(result.IsFailure);
Assert.IsInstanceOfType(result.Exception, typeof(FormatException));
}
/// <summary>
/// 测试:TryAsync 异步操作
/// </summary>
[TestMethod]
public async Task TryAsync_SuccessfulTask_ReturnsSuccess()
{
// Act
var result = await Result.TryAsync(async () =>
{
await Task.Delay(10);
});
// Assert
Assert.IsTrue(result.IsSuccess);
}
/// <summary>
/// 测试:TryAsync 异步操作返回值
/// </summary>
[TestMethod]
public async Task TryAsync_SuccessfulTaskWithValue_ReturnsSuccessWithValue()
{
// Act
var result = await Result.TryAsync(async () =>
{
await Task.Delay(10);
return 42;
});
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual(42, result.Value);
}
#endregion
#region 值提取测试
/// <summary>
/// 测试:GetOrNull 获取引用类型
/// </summary>
[TestMethod]
public void GetOrNull_SuccessResult_ReturnsValue()
{
// Arrange
var result = Result.Success("test");
// Act
var value = result.GetOrNull();
// Assert
Assert.AreEqual("test", value);
}
/// <summary>
/// 测试:GetOrNull 失败返回 null
/// </summary>
[TestMethod]
public void GetOrNull_FailureResult_ReturnsNull()
{
// Arrange
var result = Result.Failure<string>("error");
// Act
var value = result.GetOrNull();
// Assert
Assert.IsNull(value);
}
/// <summary>
/// 测试:GetOrNullable 获取值类型
/// </summary>
[TestMethod]
public void GetOrNullable_SuccessResult_ReturnsValue()
{
// Arrange
var result = Result.Success(42);
// Act
var value = result.GetOrNullable();
// Assert
Assert.AreEqual(42, value);
}
/// <summary>
/// 测试:GetOrDefault 获取默认值
/// </summary>
[TestMethod]
public void GetOrDefault_FailureResult_ReturnsDefault()
{
// Arrange
var result = Result.Failure<int>("error");
// Act
var value = result.GetOrDefault(999);
// Assert
Assert.AreEqual(999, value);
}
/// <summary>
/// 测试:GetOrThrow 成功时返回值
/// </summary>
[TestMethod]
public void GetOrThrow_SuccessResult_ReturnsValue()
{
// Arrange
var result = Result.Success(42);
// Act
var value = result.GetOrThrow();
// Assert
Assert.AreEqual(42, value);
}
/// <summary>
/// 测试:GetOrThrow 失败时抛出异常
/// </summary>
[TestMethod]
public void GetOrThrow_FailureResult_ThrowsException()
{
// Arrange
var exception = new InvalidOperationException("test error");
var result = Result.Failure<int>(exception);
// Act & Assert
var thrown = Assert.ThrowsException<InvalidOperationException>(() =>
{
result.GetOrThrow();
});
Assert.AreSame(exception, thrown);
}
/// <summary>
/// 测试:GetOrElse 使用函数提供替代值
/// </summary>
[TestMethod]
public void GetOrElse_FailureResult_CallsAlternativeProvider()
{
// Arrange
var result = Result.Failure<int>("error");
// Act
var value = result.GetOrElse(() => 100);
// Assert
Assert.AreEqual(100, value);
}
#endregion
#region 函数式操作测试
/// <summary>
/// 测试:Map 转换成功的值
/// </summary>
[TestMethod]
public void Map_SuccessResult_TransformsValue()
{
// Arrange
var result = Result.Success(10);
// Act
var mapped = result.Map(x => x * 2);
// Assert
Assert.IsTrue(mapped.IsSuccess);
Assert.AreEqual(20, mapped.Value);
}
/// <summary>
/// 测试:Map 传播失败
/// </summary>
[TestMethod]
public void Map_FailureResult_PropagatesFailure()
{
// Arrange
var result = Result.Failure<int>("error");
// Act
var mapped = result.Map(x => x * 2);
// Assert
Assert.IsTrue(mapped.IsFailure);
Assert.AreEqual("error", mapped.ErrorMessage);
}
/// <summary>
/// 测试:Bind 链式操作
/// </summary>
[TestMethod]
public void Bind_SuccessResult_ChainsOperations()
{
// Arrange
var result = Result.Success(10);
// Act
var bound = result
.Bind(x => x > 5 ? Result.Success(x * 2) : Result.Failure<int>("Too small"))
.Bind(x => Result.Success(x.ToString()));
// Assert
Assert.IsTrue(bound.IsSuccess);
Assert.AreEqual("20", bound.Value);
}
/// <summary>
/// 测试:Bind 中断失败链
/// </summary>
[TestMethod]
public void Bind_FailureInChain_StopsExecution()
{
// Arrange
var result = Result.Success(3);
var secondBindExecuted = false;
// Act
var bound = result
.Bind(x => x > 5 ? Result.Success(x * 2) : Result.Failure<int>("Too small"))
.Bind(x =>
{
secondBindExecuted = true;
return Result.Success(x * 2);
});
// Assert
Assert.IsTrue(bound.IsFailure);
Assert.IsFalse(secondBindExecuted); // 第二个 Bind 不应该执行
Assert.AreEqual("Too small", bound.ErrorMessage);
}
/// <summary>
/// 测试:Where 过滤值
/// </summary>
[TestMethod]
public void Where_PredicateSatisfied_ReturnsSuccess()
{
// Arrange
var result = Result.Success(10);
// Act
var filtered = result.Where(x => x > 5, "Value must be greater than 5");
// Assert
Assert.IsTrue(filtered.IsSuccess);
Assert.AreEqual(10, filtered.Value);
}
/// <summary>
/// 测试:Where 过滤失败
/// </summary>
[TestMethod]
public void Where_PredicateNotSatisfied_ReturnsFailure()
{
// Arrange
var result = Result.Success(3);
// Act
var filtered = result.Where(x => x > 5, "Value must be greater than 5");
// Assert
Assert.IsTrue(filtered.IsFailure);
Assert.AreEqual("Value must be greater than 5", filtered.ErrorMessage);
}
/// <summary>
/// 测试:Ensure 验证
/// </summary>
[TestMethod]
public void Ensure_ValidationPasses_ReturnsSuccess()
{
// Arrange
var result = Result.Success("test@example.com");
// Act
var ensured = result
.Ensure(email => email.Contains("@"), "Invalid email format")
.Ensure(email => email.Length > 5, "Email too short");
// Assert
Assert.IsTrue(ensured.IsSuccess);
}
/// <summary>
/// 测试:Ensure 验证失败
/// </summary>
[TestMethod]
public void Ensure_ValidationFails_ReturnsFailure()
{
// Arrange
var result = Result.Success("invalid");
// Act
var ensured = result.Ensure(email => email.Contains("@"), "Invalid email format");
// Assert
Assert.IsTrue(ensured.IsFailure);
Assert.AreEqual("Invalid email format", ensured.ErrorMessage);
}
#endregion
#region 条件执行测试
/// <summary>
/// 测试:OnSuccess 成功时执行
/// </summary>
[TestMethod]
public void OnSuccess_SuccessResult_ExecutesAction()
{
// Arrange
var result = Result.Success(42);
var executed = false;
var capturedValue = 0;
// Act
result.OnSuccess(value =>
{
executed = true;
capturedValue = value;
});
// Assert
Assert.IsTrue(executed);
Assert.AreEqual(42, capturedValue);
}
/// <summary>
/// 测试:OnSuccess 失败时不执行
/// </summary>
[TestMethod]
public void OnSuccess_FailureResult_DoesNotExecute()
{
// Arrange
var result = Result.Failure<int>("error");
var executed = false;
// Act
result.OnSuccess(value => executed = true);
// Assert
Assert.IsFalse(executed);
}
/// <summary>
/// 测试:OnFailure 失败时执行
/// </summary>
[TestMethod]
public void OnFailure_FailureResult_ExecutesAction()
{
// Arrange
var result = Result.Failure<int>(404L, "Not found");
var executed = false;
string? capturedMessage = null;
long? capturedCode = null;
// Act
result.OnFailure((ex, msg, code) =>
{
executed = true;
capturedMessage = msg;
capturedCode = code;
});
// Assert
Assert.IsTrue(executed);
Assert.AreEqual("Not found", capturedMessage);
Assert.AreEqual(404L, capturedCode);
}
/// <summary>
/// 测试:Match 模式匹配
/// </summary>
[TestMethod]
public void Match_SuccessResult_CallsSuccessHandler()
{
// Arrange
var result = Result.Success(42);
// Act
var message = result.Match(
onSuccess: value => $"Success: {value}",
onFailure: (ex, msg) => $"Error: {msg}"
);
// Assert
Assert.AreEqual("Success: 42", message);
}
/// <summary>
/// 测试:Match 失败结果
/// </summary>
[TestMethod]
public void Match_FailureResult_CallsFailureHandler()
{
// Arrange
var result = Result.Failure<int>("Something went wrong");
// Act
var message = result.Match(
onSuccess: value => $"Success: {value}",
onFailure: (ex, msg) => $"Error: {msg}"
);
// Assert
Assert.AreEqual("Error: Something went wrong", message);
}
#endregion
#region LINQ 查询语法测试
/// <summary>
/// 测试:使用 LINQ 查询语法组合 Result
/// </summary>
[TestMethod]
public void LinqQuery_CombinesResults()
{
// Arrange
var result1 = Result.Success(5);
var result2 = Result.Success(10);
// Act: 使用 LINQ 查询语法
var combined = from x in result1
from y in result2
where x > 0 && y > 0
select x + y;
// Assert
Assert.IsTrue(combined.IsSuccess);
Assert.AreEqual(15, combined.Value);
}
/// <summary>
/// 测试:LINQ 查询中的失败传播
/// </summary>
[TestMethod]
public void LinqQuery_PropagatesFailure()
{
// Arrange
var result1 = Result.Success(5);
var result2 = Result.Failure<int>("Error in result2");
// Act
var combined = from x in result1
from y in result2
select x + y;
// Assert
Assert.IsTrue(combined.IsFailure);
Assert.AreEqual("Error in result2", combined.ErrorMessage);
}
#endregion
#region 错误码相关测试
/// <summary>
/// 测试:HasErrorCode 检查错误码
/// </summary>
[TestMethod]
public void HasErrorCode_MatchingCode_ReturnsTrue()
{
// Arrange
var result = Result.Failure<int>(404L, "Not found");
// Act & Assert
Assert.IsTrue(result.HasErrorCode(404L));
Assert.IsFalse(result.HasErrorCode(500L));
}
/// <summary>
/// 测试:GetErrorCodeOrDefault 获取错误码
/// </summary>
[TestMethod]
public void GetErrorCodeOrDefault_ReturnsCodeOrDefault()
{
// Arrange
var failureResult = Result.Failure<int>(404L, "Not found");
var successResult = Result.Success(42);
// Act & Assert
Assert.AreEqual(404L, failureResult.GetErrorCodeOrDefault());
Assert.AreEqual(-1L, successResult.GetErrorCodeOrDefault());
Assert.AreEqual(999L, successResult.GetErrorCodeOrDefault(999L));
}
/// <summary>
/// 测试:FromExceptionExpand 转换自定义异常
/// </summary>
[TestMethod]
public void FromExceptionExpand_CreatesResultWithErrorCode()
{
// Arrange
var exception = new ExceptionExpand(1001L, "Business error", "Check input");
// Act
var result = ResultExtensions.FromExceptionExpand<string>(exception);
// Assert
Assert.IsTrue(result.IsFailure);
Assert.AreEqual(1001L, result.ErrorCode);
Assert.AreEqual("Business error", result.ErrorMessage);
Assert.AreSame(exception, result.Exception);
}
#endregion
#region 异步操作测试
/// <summary>
/// 测试:MapAsync 异步转换
/// </summary>
[TestMethod]
public async Task MapAsync_TransformsValueAsynchronously()
{
// Arrange
var result = Task.FromResult(Result.Success(10));
// Act
var mapped = await result.MapAsync(async value =>
{
await Task.Delay(10);
return value * 2;
});
// Assert
Assert.IsTrue(mapped.IsSuccess);
Assert.AreEqual(20, mapped.Value);
}
/// <summary>
/// 测试:BindAsync 异步链式操作
/// </summary>
[TestMethod]
public async Task BindAsync_ChainsAsynchronously()
{
// Arrange
var result = Task.FromResult(Result.Success(10));
// Act
var bound = await result.BindAsync(async value =>
{
await Task.Delay(10);
return value > 5 ? Result.Success(value * 2) : Result.Failure<int>("Too small");
});
// Assert
Assert.IsTrue(bound.IsSuccess);
Assert.AreEqual(20, bound.Value);
}
/// <summary>
/// 测试:DoOnSuccessAsync 异步副作用
/// </summary>
[TestMethod]
public async Task DoOnSuccessAsync_ExecutesAsyncAction()
{
// Arrange
var result = Task.FromResult(Result.Success(42));
var executed = false;
// Act
await result.DoOnSuccessAsync(async value =>
{
await Task.Delay(10);
executed = true;
});
// Assert
Assert.IsTrue(executed);
}
#endregion
#region 实际应用场景测试
/// <summary>
/// 场景测试:验证用户输入
/// 演示多步骤验证链
/// </summary>
[TestMethod]
public void Scenario_ValidateUserInput()
{
// Act: 验证有效输入
var validResult = ValidateEmail("user@example.com");
// Assert
Assert.IsTrue(validResult.IsSuccess);
Assert.AreEqual("user@example.com", validResult.Value);
// Act: 验证无效输入
var invalidResult = ValidateEmail("invalid");
// Assert
Assert.IsTrue(invalidResult.IsFailure);
Assert.AreEqual("Invalid email format", invalidResult.ErrorMessage);
}
/// <summary>
/// 场景测试:数据库操作链
/// 演示如何使用 Result 处理可能失败的操作序列
/// </summary>
[TestMethod]
public void Scenario_DatabaseOperationChain()
{
// Act: 成功的操作链
var successResult = FindUserById(1)
.Bind(user => UpdateUserName(user, "NewName"))
.Bind(user => SaveUser(user))
.Map(user => $"User {user.Name} saved successfully");
// Assert
Assert.IsTrue(successResult.IsSuccess);
Assert.AreEqual("User NewName saved successfully", successResult.Value);
// Act: 失败的操作链(用户不存在)
var notFoundResult = FindUserById(999)
.Bind(user => UpdateUserName(user, "NewName"))
.Bind(user => SaveUser(user))
.Map(user => $"User {user.Name} saved successfully");
// Assert
Assert.IsTrue(notFoundResult.IsFailure);
Assert.IsTrue(notFoundResult.HasErrorCode(404L));
}
/// <summary>
/// 场景测试:错误恢复
/// 演示使用 OrElse 提供备用逻辑
/// </summary>
[TestMethod]
public void Scenario_ErrorRecovery()
{
// Act: 主操作失败,使用备用操作
var result = TryPrimaryOperation()
.OrElse(() => TryFallbackOperation())
.OrElse(() => Result.Success("Default value"));
// Assert
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual("Fallback success", result.Value);
}
#endregion
#region 测试辅助方法
private Result<string> ValidateEmail(string email)
{
return email.AsResult()
.Ensure(e => !string.IsNullOrEmpty(e), "Email cannot be empty")
.Ensure(e => e.Contains("@"), "Invalid email format")
.Ensure(e => e.Length >= 5, "Email too short");
}
private class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
private Result<User> FindUserById(int id)
{
return id == 1
? Result.Success(new User { Id = 1, Name = "John" })
: Result.Failure<User>(404L, "User not found");
}
private Result<User> UpdateUserName(User user, string newName)
{
user.Name = newName;
return Result.Success(user);
}
private Result<User> SaveUser(User user)
{
// 模拟保存成功
return Result.Success(user);
}
private Result<string> TryPrimaryOperation()
{
return Result.Failure<string>("Primary operation failed");
}
private Result<string> TryFallbackOperation()
{
return Result.Success("Fallback success");
}
#endregion
}
+392
View File
@@ -0,0 +1,392 @@
using Common.ExpandFun;
namespace Common.Test;
/// <summary>
/// StringExpand 字符串扩展方法测试
/// 展示字符串相关的实用扩展方法
/// </summary>
[TestClass]
public class StringExpandTests
{
#region IsValidUrl 测试
/// <summary>
/// 测试:有效的 HTTP URL
/// </summary>
[TestMethod]
public void IsValidUrl_ValidHttpUrl_ReturnsTrue()
{
// Arrange
var url = "http://www.example.com";
// Act
var isValid = url.IsValidUrl();
// Assert
Assert.IsTrue(isValid);
}
/// <summary>
/// 测试:有效的 HTTPS URL
/// </summary>
[TestMethod]
public void IsValidUrl_ValidHttpsUrl_ReturnsTrue()
{
// Arrange
var url = "https://www.example.com/path/to/resource?query=value";
// Act
var isValid = url.IsValidUrl();
// Assert
Assert.IsTrue(isValid);
}
/// <summary>
/// 测试:有效的 FTP URL
/// </summary>
[TestMethod]
public void IsValidUrl_ValidFtpUrl_ReturnsTrue()
{
// Arrange
var url = "ftp://ftp.example.com/file.txt";
// Act
var isValid = url.IsValidUrl();
// Assert
Assert.IsTrue(isValid);
}
/// <summary>
/// 测试:无效的 URL
/// </summary>
[TestMethod]
public void IsValidUrl_InvalidUrl_ReturnsFalse()
{
// Arrange
var invalidUrls = new[]
{
"not a url",
"example.com", // 缺少协议
"http://", // 不完整
"", // 空字符串
"://invalid" // 无效格式
};
// Act & Assert
foreach (var url in invalidUrls)
{
Assert.IsFalse(url.IsValidUrl(), $"'{url}' should be invalid");
}
}
#endregion
#region ToInt 和 TryToInt 测试
/// <summary>
/// 测试:有效的整数字符串转换
/// </summary>
[TestMethod]
public void ToInt_ValidIntegerString_ReturnsInteger()
{
// Arrange & Act & Assert
Assert.AreEqual(123, "123".ToInt());
Assert.AreEqual(-456, "-456".ToInt());
Assert.AreEqual(0, "0".ToInt());
}
/// <summary>
/// 测试:无效字符串返回默认值
/// </summary>
[TestMethod]
public void ToInt_InvalidString_ReturnsDefault()
{
// Arrange & Act & Assert
Assert.AreEqual(0, "invalid".ToInt());
Assert.AreEqual(0, "12.34".ToInt());
Assert.AreEqual(999, "abc".ToInt(999));
}
/// <summary>
/// 测试:空字符串和 null 返回默认值
/// </summary>
[TestMethod]
public void ToInt_NullOrEmpty_ReturnsDefault()
{
// Arrange & Act & Assert
Assert.AreEqual(0, ((string)null!).ToInt());
Assert.AreEqual(0, "".ToInt());
Assert.AreEqual(0, " ".ToInt());
Assert.AreEqual(100, ((string)null!).ToInt(100));
}
/// <summary>
/// 测试:TryToInt 成功转换
/// </summary>
[TestMethod]
public void TryToInt_ValidString_ReturnsValue()
{
// Arrange & Act
var result = "42".TryToInt();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(42, result.Value);
}
/// <summary>
/// 测试:TryToInt 转换失败返回 null
/// </summary>
[TestMethod]
public void TryToInt_InvalidString_ReturnsNull()
{
// Arrange & Act
var result1 = "invalid".TryToInt();
var result2 = ((string)null!).TryToInt();
var result3 = "".TryToInt();
// Assert
Assert.IsNull(result1);
Assert.IsNull(result2);
Assert.IsNull(result3);
}
/// <summary>
/// 测试:整数溢出
/// </summary>
[TestMethod]
public void ToInt_Overflow_ReturnsDefault()
{
// Arrange: int.MaxValue + 1
var overflowString = "2147483648";
// Act
var result = overflowString.ToInt(-1);
// Assert: 溢出应该返回默认值
Assert.AreEqual(-1, result);
}
#endregion
#region ToDouble 和 TryToDouble 测试
/// <summary>
/// 测试:有效的浮点数字符串转换
/// </summary>
[TestMethod]
public void ToDouble_ValidDoubleString_ReturnsDouble()
{
// Arrange & Act & Assert
Assert.AreEqual(123.456, "123.456".ToDouble());
Assert.AreEqual(-789.12, "-789.12".ToDouble());
Assert.AreEqual(0.0, "0".ToDouble());
Assert.AreEqual(3.14, "3.14".ToDouble());
}
/// <summary>
/// 测试:整数字符串也能转换为 double
/// </summary>
[TestMethod]
public void ToDouble_IntegerString_ReturnsDouble()
{
// Arrange & Act
var result = "42".ToDouble();
// Assert
Assert.AreEqual(42.0, result);
}
/// <summary>
/// 测试:无效字符串返回默认值
/// </summary>
[TestMethod]
public void ToDouble_InvalidString_ReturnsDefault()
{
// Arrange & Act & Assert
Assert.AreEqual(0.0, "invalid".ToDouble());
Assert.AreEqual(999.9, "abc".ToDouble(999.9));
}
/// <summary>
/// 测试:空字符串和 null 返回默认值
/// </summary>
[TestMethod]
public void ToDouble_NullOrEmpty_ReturnsDefault()
{
// Arrange & Act & Assert
Assert.AreEqual(0.0, ((string)null!).ToDouble());
Assert.AreEqual(0.0, "".ToDouble());
Assert.AreEqual(0.0, " ".ToDouble());
Assert.AreEqual(100.5, ((string)null!).ToDouble(100.5));
}
/// <summary>
/// 测试:TryToDouble 成功转换
/// </summary>
[TestMethod]
public void TryToDouble_ValidString_ReturnsValue()
{
// Arrange & Act
var result = "3.14159".TryToDouble();
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3.14159, result.Value);
}
/// <summary>
/// 测试:TryToDouble 转换失败返回 null
/// </summary>
[TestMethod]
public void TryToDouble_InvalidString_ReturnsNull()
{
// Arrange & Act
var result1 = "invalid".TryToDouble();
var result2 = ((string)null!).TryToDouble();
var result3 = "".TryToDouble();
// Assert
Assert.IsNull(result1);
Assert.IsNull(result2);
Assert.IsNull(result3);
}
/// <summary>
/// 测试:科学计数法
/// </summary>
[TestMethod]
public void ToDouble_ScientificNotation_ConvertsCorrectly()
{
// Arrange & Act & Assert
Assert.AreEqual(1.23e5, "1.23e5".ToDouble());
Assert.AreEqual(1.23e-5, "1.23e-5".ToDouble());
}
#endregion
#region 实际应用场景测试
/// <summary>
/// 场景测试:解析配置文件值
/// </summary>
[TestMethod]
public void Scenario_ParseConfigurationValues()
{
// Arrange: 模拟从配置文件读取的字符串值
var config = new Dictionary<string, string>
{
["Port"] = "8080",
["Timeout"] = "30",
["MaxRetries"] = "invalid", // 无效值
["Price"] = "99.99",
["Discount"] = "0.15"
};
// Act: 解析配置值
var port = config["Port"].ToInt(80); // 默认 80
var timeout = config["Timeout"].ToInt(60); // 默认 60
var maxRetries = config["MaxRetries"].ToInt(3); // 无效值,使用默认 3
var price = config["Price"].ToDouble(0.0);
var discount = config["Discount"].ToDouble(0.0);
// Assert
Assert.AreEqual(8080, port);
Assert.AreEqual(30, timeout);
Assert.AreEqual(3, maxRetries); // 使用了默认值
Assert.AreEqual(99.99, price);
Assert.AreEqual(0.15, discount);
}
/// <summary>
/// 场景测试:验证用户输入的 URL
/// </summary>
[TestMethod]
public void Scenario_ValidateUserInputUrl()
{
// Arrange: 模拟用户输入
var userInputs = new[]
{
"https://www.google.com",
"not a url",
"ftp://files.example.com",
"example.com"
};
// Act: 验证 URL
var validUrls = userInputs.Where(url => url.IsValidUrl()).ToList();
// Assert
Assert.AreEqual(2, validUrls.Count);
Assert.IsTrue(validUrls.Contains("https://www.google.com"));
Assert.IsTrue(validUrls.Contains("ftp://files.example.com"));
}
/// <summary>
/// 场景测试:安全地解析用户输入的数字
/// </summary>
[TestMethod]
public void Scenario_SafelyParseUserInput()
{
// Arrange: 模拟用户输入(可能无效)
string? userAgeInput = "25";
string? userWeightInput = "invalid";
string? userHeightInput = null;
// Act: 安全解析
var age = userAgeInput.TryToInt();
var weight = userWeightInput.TryToDouble();
var height = userHeightInput.TryToDouble();
// Assert
Assert.IsNotNull(age);
Assert.AreEqual(25, age.Value);
Assert.IsNull(weight); // 无效输入
Assert.IsNull(height); // null 输入
// 使用默认值的版本
var ageWithDefault = userAgeInput.ToInt(18);
var weightWithDefault = userWeightInput.ToDouble(70.0);
var heightWithDefault = userHeightInput.ToDouble(170.0);
Assert.AreEqual(25, ageWithDefault);
Assert.AreEqual(70.0, weightWithDefault); // 使用默认值
Assert.AreEqual(170.0, heightWithDefault); // 使用默认值
}
/// <summary>
/// 场景测试:处理来自外部 API 的数据
/// </summary>
[TestMethod]
public void Scenario_ParseApiResponse()
{
// Arrange: 模拟 API 返回的字符串数据
var apiData = new
{
UserId = "12345",
Score = "95.5",
ProfileUrl = "https://api.example.com/users/12345",
InvalidUrl = "not-a-url",
MissingValue = (string)null!
};
// Act: 解析数据
var userId = apiData.UserId.ToInt();
var score = apiData.Score.ToDouble();
var isValidProfileUrl = apiData.ProfileUrl.IsValidUrl();
var isValidInvalidUrl = apiData.InvalidUrl.IsValidUrl();
var missingValue = apiData.MissingValue.ToInt(-1);
// Assert
Assert.AreEqual(12345, userId);
Assert.AreEqual(95.5, score);
Assert.IsTrue(isValidProfileUrl);
Assert.IsFalse(isValidInvalidUrl);
Assert.AreEqual(-1, missingValue);
}
#endregion
}
+336
View File
@@ -0,0 +1,336 @@
using Common.Utils;
namespace Common.Test;
/// <summary>
/// SystemHelper 测试
/// 展示系统相关的实用工具方法
/// </summary>
[TestClass]
public class SystemHelperTests
{
#region GetAvailablePort 测试
/// <summary>
/// 测试:获取可用端口返回有效端口号
/// </summary>
[TestMethod]
public void GetAvailablePort_ReturnsValidPort()
{
// Act
var port = SystemHelper.GetAvailablePort();
// Assert: 端口应该在有效范围内
Assert.IsTrue(port > 0);
Assert.IsTrue(port <= 65535);
}
/// <summary>
/// 测试:多次调用获取不同的可用端口
/// </summary>
[TestMethod]
public void GetAvailablePort_MultipleCallsReturnDifferentPorts()
{
// Act: 多次获取端口
var port1 = SystemHelper.GetAvailablePort();
var port2 = SystemHelper.GetAvailablePort();
var port3 = SystemHelper.GetAvailablePort();
// Assert: 每次应该返回不同的端口(由操作系统分配)
var ports = new[] { port1, port2, port3 };
Assert.AreEqual(3, ports.Distinct().Count(), "Should get different ports");
}
/// <summary>
/// 测试:获取的端口确实可用
/// 演示如何验证端口是否真的可以使用
/// </summary>
[TestMethod]
public void GetAvailablePort_PortIsActuallyAvailable()
{
// Act: 获取可用端口
var port = SystemHelper.GetAvailablePort();
// Assert: 尝试在该端口上创建监听器
using var listener = new System.Net.Sockets.TcpListener(
System.Net.IPAddress.Loopback,
port
);
// 如果端口被占用,Start 会抛出异常
listener.Start();
listener.Stop();
// 如果没有抛出异常,说明端口确实可用
Assert.IsTrue(true);
}
#endregion
#region IsAdministrator 测试
/// <summary>
/// 测试:IsAdministrator 在 Windows 上正常工作
/// 注意:此测试在非 Windows 平台会被跳过
/// </summary>
[TestMethod]
public void IsAdministrator_OnWindows_ReturnsBoolean()
{
// Arrange: 检查是否为 Windows 平台
if (!OperatingSystem.IsWindows())
{
Assert.Inconclusive("This test only runs on Windows");
return;
}
// Act
var isAdmin = SystemHelper.IsAdministrator();
// Assert: 应该返回布尔值(无论是 true 还是 false 都是有效的)
Assert.IsInstanceOfType(isAdmin, typeof(bool));
// 输出结果供参考
Console.WriteLine($"Running as administrator: {isAdmin}");
}
/// <summary>
/// 测试:IsAdministrator 在非 Windows 平台抛出异常
/// 注意:此测试在 Windows 平台会被跳过
/// </summary>
[TestMethod]
public void IsAdministrator_OnNonWindows_ThrowsException()
{
// Arrange: 检查是否为非 Windows 平台
if (OperatingSystem.IsWindows())
{
Assert.Inconclusive("This test only runs on non-Windows platforms");
return;
}
// Act & Assert: 在非 Windows 平台应该抛出异常
var exception = Assert.ThrowsException<PlatformNotSupportedException>(() =>
{
SystemHelper.IsAdministrator();
});
Assert.IsTrue(exception.Message.Contains("Windows"));
}
#endregion
#region 实际应用场景测试
/// <summary>
/// 场景测试:为开发服务器分配端口
/// 演示如何在启动服务器时动态分配端口
/// </summary>
[TestMethod]
public void Scenario_AllocatePortForDevelopmentServer()
{
// Arrange: 需要启动多个开发服务器
var serverCount = 3;
var allocatedPorts = new List<int>();
// Act: 为每个服务器分配端口
for (int i = 0; i < serverCount; i++)
{
var port = SystemHelper.GetAvailablePort();
allocatedPorts.Add(port);
Console.WriteLine($"Server {i + 1} allocated port: {port}");
}
// Assert: 所有端口都应该是唯一的
Assert.AreEqual(serverCount, allocatedPorts.Distinct().Count());
Assert.IsTrue(allocatedPorts.All(p => p > 0 && p <= 65535));
}
/// <summary>
/// 场景测试:检查权限后执行特权操作
/// 演示如何在需要管理员权限的功能中使用 IsAdministrator
/// </summary>
[TestMethod]
public void Scenario_CheckAdminBeforePrivilegedOperation()
{
// 跳过非 Windows 平台
if (!OperatingSystem.IsWindows())
{
Assert.Inconclusive("This test only runs on Windows");
return;
}
// Arrange: 定义需要管理员权限的操作
void PrivilegedOperation()
{
if (!SystemHelper.IsAdministrator())
{
throw new UnauthorizedAccessException(
"This operation requires administrator privileges"
);
}
// 执行需要管理员权限的操作...
Console.WriteLine("Executing privileged operation");
}
// Act & Assert: 根据当前权限决定行为
if (SystemHelper.IsAdministrator())
{
// 如果是管理员,操作应该成功
PrivilegedOperation();
Assert.IsTrue(true);
}
else
{
// 如果不是管理员,应该抛出异常
Assert.ThrowsException<UnauthorizedAccessException>(() =>
{
PrivilegedOperation();
});
}
}
/// <summary>
/// 场景测试:动态端口绑定用于测试
/// 演示如何在集成测试中使用动态端口避免冲突
/// </summary>
[TestMethod]
public void Scenario_DynamicPortForIntegrationTesting()
{
// Arrange: 模拟需要启动测试服务器的场景
var testServers = new List<TestServer>();
try
{
// Act: 启动多个测试服务器
for (int i = 0; i < 3; i++)
{
var port = SystemHelper.GetAvailablePort();
var server = new TestServer($"TestServer{i + 1}", port);
server.Start();
testServers.Add(server);
Console.WriteLine($"{server.Name} started on port {server.Port}");
}
// Assert: 所有服务器都应该成功启动
Assert.AreEqual(3, testServers.Count);
Assert.IsTrue(testServers.All(s => s.IsRunning));
// 验证端口都不相同
var ports = testServers.Select(s => s.Port).ToList();
Assert.AreEqual(3, ports.Distinct().Count());
}
finally
{
// Cleanup: 停止所有测试服务器
foreach (var server in testServers)
{
server.Stop();
}
}
}
/// <summary>
/// 场景测试:端口范围检查
/// 演示验证分配的端口在合理范围内
/// </summary>
[TestMethod]
public void Scenario_VerifyPortRange()
{
// Arrange: 获取多个端口样本
var sampleSize = 10;
var ports = new List<int>();
// Act: 收集端口样本
for (int i = 0; i < sampleSize; i++)
{
ports.Add(SystemHelper.GetAvailablePort());
}
// Assert: 验证所有端口都在合理范围内
// 动态端口范围通常是 49152-65535
Assert.IsTrue(ports.All(p => p > 0), "All ports should be positive");
Assert.IsTrue(ports.All(p => p <= 65535), "All ports should be <= 65535");
// 统计信息
Console.WriteLine($"Port range: {ports.Min()} - {ports.Max()}");
Console.WriteLine($"Average port: {ports.Average():F0}");
}
/// <summary>
/// 场景测试:跨平台兼容性处理
/// 演示如何编写跨平台友好的代码
/// </summary>
[TestMethod]
public void Scenario_CrossPlatformCompatibility()
{
// Act: 尝试检查管理员权限,优雅处理不同平台
bool? isAdmin = null;
Exception? caughtException = null;
try
{
isAdmin = SystemHelper.IsAdministrator();
}
catch (PlatformNotSupportedException ex)
{
caughtException = ex;
}
// Assert: 根据平台验证行为
if (OperatingSystem.IsWindows())
{
// Windows: 应该返回结果
Assert.IsNotNull(isAdmin);
Assert.IsNull(caughtException);
Console.WriteLine($"Windows platform - Is Admin: {isAdmin}");
}
else
{
// 非 Windows: 应该抛出异常
Assert.IsNull(isAdmin);
Assert.IsNotNull(caughtException);
Console.WriteLine($"Non-Windows platform - Exception caught: {caughtException.Message}");
}
}
#endregion
#region 测试辅助类型
/// <summary>
/// 测试用的简单服务器模拟
/// </summary>
private class TestServer
{
public string Name { get; }
public int Port { get; }
public bool IsRunning { get; private set; }
private System.Net.Sockets.TcpListener? _listener;
public TestServer(string name, int port)
{
Name = name;
Port = port;
}
public void Start()
{
_listener = new System.Net.Sockets.TcpListener(
System.Net.IPAddress.Loopback,
Port
);
_listener.Start();
IsRunning = true;
}
public void Stop()
{
_listener?.Stop();
IsRunning = false;
}
}
#endregion
}
+4
View File
@@ -16,4 +16,8 @@
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Sharp\Config\Config.csproj" />
</ItemGroup>
</Project>
+284
View File
@@ -0,0 +1,284 @@
using Microsoft.Extensions.Configuration;
using System.ComponentModel.DataAnnotations;
using System.Text;
// - 本文件覆盖 Config 模块的核心能力测试:多格式读写、强类型绑定、验证、环境变量、热更新、Hook 扩展。
// - 使用 MSTest。请确保已在测试项目中添加对 src\Sharp\Config\Config.csproj 的 ProjectReference。
// - 约定:配置项的“后添加覆盖先添加”(Configuration 的默认行为)。环境变量位于最末,优先级最高。
namespace Config.Test
{
// 为避免文件监听与并发写入导致的互相影响,禁用并行执行
[TestClass]
[DoNotParallelize]
public class ConfigTests
{
// 简单的配置模型,涵盖:基础字段、嵌套对象、DataAnnotations 验证
public class AppSettings
{
public string? Name { get; set; }
public int Port { get; set; }
[Range(1, 65535)] public int ValidPort { get; set; }
[Required] public string? RequiredField { get; set; }
public DatabaseSettings Database { get; set; } = new();
}
public class DatabaseSettings
{
[Required] public string? Host { get; set; }
public string? User { get; set; }
public string? Password { get; set; }
}
// 创建临时目录的轻量封装,便于测试隔离
private sealed class TempDirScope : IDisposable
{
public string Dir { get; }
public TempDirScope()
{
Dir = Path.Combine(Path.GetTempPath(), "ConfigTests_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Dir);
}
public string PathOf(string file) => System.IO.Path.Combine(Dir, file);
public void Dispose()
{
try
{
Directory.Delete(Dir, recursive: true);
}
catch
{
/* 文件监视可能暂占用,忽略 */
}
}
}
[TestMethod]
public void Build_FromFiles_Binds_Json_Ini_Xml_Toml_WithValidation()
{
using var scope = new TempDirScope();
// 1. 构造多格式配置文件,分别写入不同字段
var jsonPath = scope.PathOf("appsettings.json");
var iniPath = scope.PathOf("settings.ini");
var xmlPath = scope.PathOf("extra.xml");
var tomlPath = scope.PathOf("feature.toml");
File.WriteAllText(jsonPath, /* UTF-8 无 BOM */
"{\n" +
" \"Name\": \"fromJson\",\n" +
" \"Port\": 10\n" +
"}", new UTF8Encoding(false));
File.WriteAllText(iniPath,
// INI 文件:根键与一层嵌套(Database 节)
"User=iniUser\n" +
"[Database]\n" +
"Host=iniHost\n",
new UTF8Encoding(false));
File.WriteAllText(xmlPath,
// XML:顶层键直接映射
"<settings>\n" +
" <ValidPort>100</ValidPort>\n" +
"</settings>", new UTF8Encoding(false));
File.WriteAllText(tomlPath,
"RequiredField = \"fromToml\"\n",
new UTF8Encoding(false));
// 2. 构建配置(顺序:JSON -> INI -> XML -> TOML),最后添加环境变量
var cm = ConfigBuilder.BuildFromFiles(
files: new[] { "appsettings.json", "settings.ini", "extra.xml", "feature.toml" },
reloadOnChange: true,
includeEnvironment: true,
environmentPrefix: "APP_",
basePath: scope.Dir
);
// 3. 强类型绑定 + 验证(DataAnnotations)
var settings = cm.Get<AppSettings>(validate: true);
Assert.AreEqual("fromJson", settings.Name);
Assert.AreEqual(10, settings.Port);
Assert.AreEqual(100, settings.ValidPort);
Assert.AreEqual("iniHost", settings.Database.Host);
Assert.AreEqual("iniUser", settings.Database.User);
Assert.AreEqual("fromToml", settings.RequiredField);
}
[TestMethod]
public void Validation_Fails_On_Missing_Required()
{
using var scope = new TempDirScope();
// 缺少 RequiredField 与 Database.Host,期望验证失败
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"x\", \"Port\": 5, \"ValidPort\": 200 }",
new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(new[] { "appsettings.json" }, basePath: scope.Dir);
Assert.ThrowsException<ConfigurationValidationException>(() => { cm.Get<AppSettings>(validate: true); });
}
[TestMethod]
public void EnvironmentVariables_Override_FileValues()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fromFile\", \"Database\": { \"Host\": \"fileHost\" } }",
new UTF8Encoding(false));
// 使用前缀 APP_,配置键用双下划线分隔层级
Environment.SetEnvironmentVariable("APP__Name", "fromEnv");
Environment.SetEnvironmentVariable("APP__Database__Host", "envHost");
try
{
var cm = ConfigBuilder.BuildFromFiles(
new[] { "appsettings.json" },
basePath: scope.Dir,
includeEnvironment: true,
environmentPrefix: "APP_"
);
var settings = cm.Get<AppSettings>();
Assert.AreEqual("fromEnv", settings.Name);
Assert.AreEqual("envHost", settings.Database.Host);
}
finally
{
// 清理环境变量,避免影响其他测试
Environment.SetEnvironmentVariable("APP__Name", null);
Environment.SetEnvironmentVariable("APP__Database__Host", null);
}
}
[TestMethod]
[Timeout(15000)]
public async Task HotReload_FileChange_Triggers_Callback_And_Rebind()
{
using var scope = new TempDirScope();
var json = scope.PathOf("hot.json");
File.WriteAllText(json, "{ \"Port\": 1 }", new UTF8Encoding(false));
var cm = ConfigBuilder.BuildFromFiles(
new[] { "hot.json" },
reloadOnChange: true,
basePath: scope.Dir
);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
using var sub = cm.OnReload(() => tcs.TrySetResult(true));
// 初始读取
var s1 = cm.Get<AppSettings>();
Assert.AreEqual(1, s1.Port);
// 修改内容,触发文件监控
await Task.Delay(250); // 稍等,确保监控挂上
File.WriteAllText(json, "{ \"Port\": 2 }", new UTF8Encoding(false));
// 等待回调触发
var fired = await Task.WhenAny(tcs.Task, Task.Delay(5000)) == tcs.Task;
Assert.IsTrue(fired, "未在期望时间内触发热更新回调");
// 变更后再取配置
var s2 = cm.Get<AppSettings>();
Assert.AreEqual(2, s2.Port);
}
[TestMethod]
public void Save_And_Load_Json_Xml_Ini_Toml()
{
using var scope = new TempDirScope();
var settings = new AppSettings
{
Name = "save",
Port = 1234,
ValidPort = 80,
RequiredField = "req",
Database = new DatabaseSettings { Host = "db", User = "u" }
};
var cm = ConfigManager.Build(new ConfigOptions()); // 用于调用 Save/Load
// JSON
var json = scope.PathOf("a.json");
cm.Save(settings, json, ConfigFormat.Json);
var jsonBack = cm.LoadFromFile<AppSettings>(json, ConfigFormat.Json);
Assert.AreEqual("save", jsonBack!.Name);
Assert.AreEqual(1234, jsonBack.Port);
Assert.AreEqual("db", jsonBack.Database.Host);
// XML
var xml = scope.PathOf("a.xml");
cm.Save(settings, xml, ConfigFormat.Xml);
var xmlBack = cm.LoadFromFile<AppSettings>(xml, ConfigFormat.Xml);
Assert.AreEqual(80, xmlBack!.ValidPort);
// INI(支持简单与一层嵌套的序列化回读)
var ini = scope.PathOf("a.ini");
cm.Save(settings, ini, ConfigFormat.Ini);
var iniBack = cm.LoadFromFile<AppSettings>(ini, ConfigFormat.Ini);
Assert.AreEqual("u", iniBack!.Database.User);
Assert.AreEqual("db", iniBack.Database.Host);
// TOML:当前不支持直接泛型反序列化(LoadFromFile),需通过配置管线加载
var toml = scope.PathOf("a.toml");
cm.Save(settings, toml, ConfigFormat.Toml);
Assert.ThrowsException<NotSupportedException>(() =>
{
_ = cm.LoadFromFile<AppSettings>(toml, ConfigFormat.Toml);
});
var cmToml = ConfigBuilder.BuildFromFiles(new[] { "a.toml" }, basePath: scope.Dir);
var tomlBack = cmToml.Get<AppSettings>();
Assert.AreEqual("save", tomlBack.Name);
Assert.AreEqual("db", tomlBack.Database.Host);
}
[TestMethod]
public void ConfigureBuilderHook_Appends_Providers_And_Overrides()
{
using var scope = new TempDirScope();
File.WriteAllText(scope.PathOf("appsettings.json"),
"{ \"Name\": \"fileName\" }",
new UTF8Encoding(false));
var cm = ConfigBuilder.Build(opt =>
{
opt.BasePath = scope.Dir;
opt.Sources.Add(new ConfigSource
{
Path = "appsettings.json", Format = ConfigFormat.Json, Optional
= false
});
// 通过 Hook 追加内存配置,位于最后,覆盖前面的值
opt.ConfigureBuilderHook = builder =>
{
var kv = new[] { new KeyValuePair<string, string?>("Name", "hookName") };
builder.AddInMemoryCollection(kv);
return builder;
};
});
var s = cm.Get<AppSettings>();
Assert.AreEqual("hookName", s.Name, "Hook 未按预期覆盖前序配置源");
}
}
}