Skip to content

Commit 0337200

Browse files
committed
test(options): 添加代码审查修复的单元测试
测试覆盖以下修复项: - H-004: OptionsProvider 并发 GetOptions 线程安全性 - H-002: OptionsBuilder 无效默认值和类型转换容错处理 - H-003: 无效值转换时的调试输出 - C-001: volatile 字段可见性一致性 - BooleanParser: 布尔值识别和解析功能
1 parent 9dbdf39 commit 0337200

1 file changed

Lines changed: 386 additions & 0 deletions

File tree

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using GameFrameX.Foundation.Options;
5+
using GameFrameX.Foundation.Options.Attributes;
6+
using Xunit;
7+
8+
namespace GameFrameX.Foundation.Tests.Options
9+
{
10+
/// <summary>
11+
/// 针对 CODE_REVIEW 中已修复问题的单元测试
12+
/// Unit tests for issues fixed in CODE_REVIEW
13+
/// </summary>
14+
public class CodeReviewFixTests
15+
{
16+
#region 测试配置类
17+
18+
/// <summary>
19+
/// 基本测试配置
20+
/// </summary>
21+
public class BasicConfig
22+
{
23+
public string Host { get; set; } = "localhost";
24+
public int Port { get; set; } = 8080;
25+
public bool Debug { get; set; } = false;
26+
}
27+
28+
/// <summary>
29+
/// 带默认值的测试配置
30+
/// </summary>
31+
public class ConfigWithDefaults
32+
{
33+
[Option("timeout", DefaultValue = 30)]
34+
public int Timeout { get; set; }
35+
36+
[Option("retries", DefaultValue = "3")]
37+
public int Retries { get; set; }
38+
39+
[Option("enabled", DefaultValue = true)]
40+
public bool Enabled { get; set; }
41+
}
42+
43+
/// <summary>
44+
/// 带无效默认值的测试配置
45+
/// </summary>
46+
public class ConfigWithInvalidDefaults
47+
{
48+
[Option("port", DefaultValue = "not_a_number")]
49+
public int Port { get; set; } = 8080;
50+
51+
[Option("timeout", DefaultValue = "invalid")]
52+
public int Timeout { get; set; } = 30;
53+
}
54+
55+
/// <summary>
56+
/// 带必需选项的测试配置
57+
/// </summary>
58+
public class ConfigWithRequired
59+
{
60+
[Option("api-key", Required = true)]
61+
public string ApiKey { get; set; }
62+
63+
public string Host { get; set; } = "localhost";
64+
}
65+
66+
#endregion
67+
68+
#region H-004: OptionsProvider 线程安全测试
69+
70+
/// <summary>
71+
/// [H-004] 测试并发获取选项时的线程安全性
72+
/// </summary>
73+
[Fact]
74+
public void OptionsProvider_ConcurrentGetOptions_ShouldBeThreadSafe()
75+
{
76+
// 准备测试数据
77+
var args = new[] { "--host", "concurrent.example.com", "--port", "9999" };
78+
OptionsProvider.Initialize(args);
79+
80+
// 清除缓存以确保测试独立性
81+
OptionsProvider.ClearCache();
82+
83+
var exceptions = new List<Exception>();
84+
var results = new List<BasicConfig>();
85+
var lockObj = new object();
86+
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = 10 };
87+
88+
// 并发执行
89+
Parallel.For(0, 100, parallelOptions, i =>
90+
{
91+
try
92+
{
93+
var config = OptionsProvider.GetOptions<BasicConfig>(skipValidation: true, enableDebugOutput: false);
94+
lock (lockObj)
95+
{
96+
results.Add(config);
97+
}
98+
}
99+
catch (Exception ex)
100+
{
101+
lock (lockObj)
102+
{
103+
exceptions.Add(ex);
104+
}
105+
}
106+
});
107+
108+
// 验证结果
109+
Assert.Empty(exceptions);
110+
Assert.Equal(100, results.Count);
111+
112+
// 所有结果应该有相同的值
113+
foreach (var config in results)
114+
{
115+
Assert.Equal("concurrent.example.com", config.Host);
116+
Assert.Equal(9999, config.Port);
117+
}
118+
}
119+
120+
/// <summary>
121+
/// [H-004] 测试 Initialize 和 GetOptions 并发调用
122+
/// </summary>
123+
[Fact]
124+
public void OptionsProvider_ConcurrentInitializeAndGetOptions_ShouldNotThrow()
125+
{
126+
var exceptions = new List<Exception>();
127+
var lockObj = new object();
128+
129+
// 并发执行 Initialize 和 GetOptions
130+
var tasks = new Task[20];
131+
132+
for (int i = 0; i < 20; i++)
133+
{
134+
var index = i;
135+
tasks[i] = Task.Run(() =>
136+
{
137+
try
138+
{
139+
if (index % 2 == 0)
140+
{
141+
// 一半线程执行 Initialize
142+
OptionsProvider.Initialize(new[] { "--host", $"host{index}.example.com" });
143+
}
144+
else
145+
{
146+
// 一半线程执行 GetOptions
147+
_ = OptionsProvider.GetOptions<BasicConfig>(skipValidation: true, enableDebugOutput: false);
148+
}
149+
}
150+
catch (Exception ex)
151+
{
152+
lock (lockObj)
153+
{
154+
exceptions.Add(ex);
155+
}
156+
}
157+
});
158+
}
159+
160+
Task.WaitAll(tasks);
161+
162+
// 不应该有异常(线程安全)
163+
Assert.Empty(exceptions);
164+
}
165+
166+
#endregion
167+
168+
#region H-002: 异常处理改进测试
169+
170+
/// <summary>
171+
/// [H-002] 测试无效默认值时的容错处理
172+
/// </summary>
173+
[Fact]
174+
public void OptionsBuilder_InvalidDefaultValue_ShouldUseDefaultPropertyValue()
175+
{
176+
// 准备测试数据 - 不提供参数,使用默认值
177+
var args = Array.Empty<string>();
178+
179+
// 执行测试
180+
var builder = new OptionsBuilder<ConfigWithInvalidDefaults>(args);
181+
var config = builder.Build();
182+
183+
// 验证结果 - 应该使用属性的默认值,而不是特性的无效默认值
184+
Assert.NotNull(config);
185+
Assert.Equal(8080, config.Port); // 属性默认值
186+
Assert.Equal(30, config.Timeout); // 属性默认值
187+
}
188+
189+
/// <summary>
190+
/// [H-002] 测试类型转换失败时的容错处理
191+
/// </summary>
192+
[Fact]
193+
public void OptionsBuilder_TypeConversionFailure_ShouldNotThrow()
194+
{
195+
// 准备测试数据 - 提供无效的端口值
196+
var args = new[] { "--port", "not_a_number" };
197+
198+
// 执行测试 - 不应该抛出异常
199+
var builder = new OptionsBuilder<BasicConfig>(args);
200+
var config = builder.Build();
201+
202+
// 验证结果 - 应该使用默认值
203+
Assert.NotNull(config);
204+
Assert.Equal(8080, config.Port); // 默认值
205+
}
206+
207+
#endregion
208+
209+
#region H-003: 空 catch 代码块修复测试
210+
211+
/// <summary>
212+
/// [H-003] 测试无效值转换时的日志输出(确保 catch 不是空的)
213+
/// </summary>
214+
[Fact]
215+
public void OptionsBuilder_InvalidValueConversion_ShouldHandleGracefully()
216+
{
217+
// 准备测试数据 - 各种无效值
218+
var testCases = new[]
219+
{
220+
new[] { "--port", "abc" },
221+
new[] { "--port", "99999999999999999999" }, // 溢出
222+
new[] { "--port", "" },
223+
new[] { "--port", "null" },
224+
};
225+
226+
foreach (var args in testCases)
227+
{
228+
// 执行测试 - 不应该抛出异常
229+
var builder = new OptionsBuilder<BasicConfig>(args);
230+
var config = builder.Build();
231+
232+
// 验证结果 - 应该使用默认值
233+
Assert.NotNull(config);
234+
Assert.Equal(8080, config.Port); // 默认值
235+
}
236+
}
237+
238+
#endregion
239+
240+
#region C-001: volatile 关键字测试(通过 H-004 覆盖)
241+
242+
/// <summary>
243+
/// [C-001] 测试 _args 字段的可见性(volatile)
244+
/// </summary>
245+
[Fact]
246+
public void OptionsProvider_ArgsVisibility_ShouldBeConsistent()
247+
{
248+
// 初始化
249+
OptionsProvider.Initialize(new[] { "--host", "first.example.com" });
250+
OptionsProvider.ClearCache();
251+
252+
var config1 = OptionsProvider.GetOptions<BasicConfig>(enableDebugOutput: false);
253+
Assert.Equal("first.example.com", config1.Host);
254+
255+
// 重新初始化
256+
OptionsProvider.Initialize(new[] { "--host", "second.example.com" });
257+
OptionsProvider.ClearCache();
258+
259+
var config2 = OptionsProvider.GetOptions<BasicConfig>(enableDebugOutput: false);
260+
Assert.Equal("second.example.com", config2.Host);
261+
}
262+
263+
#endregion
264+
265+
#region BooleanParser 测试
266+
267+
/// <summary>
268+
/// 测试 BooleanParser.IsBooleanValue 方法
269+
/// </summary>
270+
[Theory]
271+
[InlineData("true", true)]
272+
[InlineData("false", true)]
273+
[InlineData("TRUE", true)]
274+
[InlineData("FALSE", true)]
275+
[InlineData("1", true)]
276+
[InlineData("0", true)]
277+
[InlineData("yes", true)]
278+
[InlineData("no", true)]
279+
[InlineData("on", true)]
280+
[InlineData("off", true)]
281+
[InlineData("YES", true)]
282+
[InlineData("NO", true)]
283+
[InlineData("ON", true)]
284+
[InlineData("OFF", true)]
285+
[InlineData("maybe", false)]
286+
[InlineData("", false)]
287+
[InlineData(" ", false)]
288+
public void BooleanParser_IsBooleanValue_ShouldReturnCorrectResult(string value, bool expected)
289+
{
290+
var result = BooleanParser.IsBooleanValue(value);
291+
Assert.Equal(expected, result);
292+
}
293+
294+
/// <summary>
295+
/// 测试 BooleanParser.ParseBooleanValue 方法
296+
/// </summary>
297+
[Theory]
298+
[InlineData("true", true)]
299+
[InlineData("TRUE", true)]
300+
[InlineData("1", true)]
301+
[InlineData("yes", true)]
302+
[InlineData("on", true)]
303+
[InlineData("false", false)]
304+
[InlineData("FALSE", false)]
305+
[InlineData("0", false)]
306+
[InlineData("no", false)]
307+
[InlineData("off", false)]
308+
[InlineData("", false)]
309+
[InlineData(" ", false)]
310+
[InlineData("invalid", false)]
311+
public void BooleanParser_ParseBooleanValue_ShouldReturnCorrectResult(string value, bool expected)
312+
{
313+
var result = BooleanParser.ParseBooleanValue(value);
314+
Assert.Equal(expected, result);
315+
}
316+
317+
/// <summary>
318+
/// 测试 BooleanParser 与 OptionsBuilder 的集成
319+
/// 注意: 在 Flag 模式下(默认),布尔选项被视为标志,不需要显式值
320+
/// </summary>
321+
[Fact]
322+
public void OptionsBuilder_WithBooleanValues_ShouldParseCorrectly()
323+
{
324+
// 在 Flag 模式下,布尔选项作为标志处理
325+
// --debug 存在即为 true,不存在则使用默认值 (false)
326+
327+
// 测试 1: 存在标志 -> true
328+
var builder1 = new OptionsBuilder<BasicConfig>(new[] { "--debug" });
329+
var config1 = builder1.Build();
330+
Assert.True(config1.Debug);
331+
332+
// 测试 2: 不存在标志 -> 默认值 (false)
333+
var builder2 = new OptionsBuilder<BasicConfig>(Array.Empty<string>());
334+
var config2 = builder2.Build();
335+
Assert.False(config2.Debug);
336+
337+
// 测试 3: 使用键值对格式
338+
var builder3 = new OptionsBuilder<BasicConfig>(new[] { "--debug=true" });
339+
var config3 = builder3.Build();
340+
Assert.True(config3.Debug);
341+
342+
// 测试 4: 使用键值对格式设置为 false
343+
var builder4 = new OptionsBuilder<BasicConfig>(new[] { "--debug=false" });
344+
var config4 = builder4.Build();
345+
Assert.False(config4.Debug);
346+
}
347+
348+
#endregion
349+
350+
#region 综合测试
351+
352+
/// <summary>
353+
/// 测试所有修复的集成场景
354+
/// </summary>
355+
[Fact]
356+
public void CodeReviewFixes_IntegrationTest_ShouldWorkCorrectly()
357+
{
358+
// 准备测试数据
359+
var args = new[] { "--host", "integration.example.com", "--port", "8888", "--debug", "yes" };
360+
361+
// 初始化
362+
OptionsProvider.Initialize(args);
363+
OptionsProvider.ClearCache();
364+
365+
// 并发获取配置
366+
var tasks = new Task<BasicConfig>[5];
367+
for (int i = 0; i < 5; i++)
368+
{
369+
tasks[i] = Task.Run(() => OptionsProvider.GetOptions<BasicConfig>(enableDebugOutput: false));
370+
}
371+
372+
Task.WaitAll(tasks);
373+
374+
// 验证所有任务返回相同的结果
375+
foreach (var task in tasks)
376+
{
377+
var config = task.Result;
378+
Assert.Equal("integration.example.com", config.Host);
379+
Assert.Equal(8888, config.Port);
380+
Assert.True(config.Debug);
381+
}
382+
}
383+
384+
#endregion
385+
}
386+
}

0 commit comments

Comments
 (0)