抽象测试基类实现

开发背景

在开发自动化测试工具时,我们经常面临对测试用例进行循环压力测试的需求。由于测试类的重复编写工作量较大,因此计划开发一个抽象基类来简化这一过程。

测试流程

测试流程大致如下图所示。

在整个测试流程中存在一个大循环(circle)。首先,执行带有@BeforeClass注解的方法;接着,针对每个测试用例(即带有@Test注解的方法)进行循环测试,在每个测试用例的循环内部,又有一个针对该测试用例自身的循环次数(count)。在这个内部循环中,首先执行带有@Before注解的方法,然后执行具体的测试逻辑,最后执行带有@After注解的方法。

测试结果

通过TestResult存储单个测试结果的信息,包括如下内容:

  • Id:唯一标识
  • Circle:测试循环次数
  • Case:测试用例名称
  • Description:测试用例描述
  • Count:测试用例执行次数
  • Status:测试状态,成功/失败
  • Message:失败时的错误信息

功能实现

测试属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>[test]</summary>
[AttributeUsage(AttributeTargets.Method)]
public class TestAttribute : Attribute
{
public int Grade { get; set; }
public int Count { get; set; }
public string Description { get; set; }

public TestAttribute(int grade = int.MaxValue, int count = 1, string description = "null")
{
Grade = grade;
Count = count;
Description = description;
}
}
  • TestAttribute:用于标记测试方法,包含 Grade(测试等级,数值越小优先执行)、Count(测试执行次数)和 Description(测试描述)属性。

方法注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>[Before]</summary>
[AttributeUsage(AttributeTargets.Method)]
public class BeforeAttribute : Attribute { }

/// <summary>[BeforeClass]</summary>
[AttributeUsage(AttributeTargets.Method)]
public class BeforeClassAttribute : Attribute { }

/// <summary>[After]</summary>
[AttributeUsage(AttributeTargets.Method)]
public class AfterAttribute : Attribute { }

/// <summary>[AfterClass]</summary>
[AttributeUsage(AttributeTargets.Method)]
public class AfterClassAttribute : Attribute { }
  • BeforeAttributeBeforeClassAttributeAfterAttributeAfterClassAttribute:用于标记在测试前后和测试类前后需要执行的方法。

测试结果

1
2
3
4
5
6
7
8
9
10
11
/// <summary>存储测试结果</summary>
public class TestResult
{
public int Id { get; set; } // id
public int Circle { get; set; } // 循环次数
public string Case { get; set; } // 测试用例名称
public string Description { get; set; } // 测试用例描述
public int Count { get; set; } // 当前用例的执行次数
public string Status { get; set; } // 测试状态,成功或失败
public string Message { get; set; } // 失败时的错误信息
}

抽象基类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
/// <summary>抽象测试类</summary>
public abstract class TestBase(int circle)
{
private int _circle = circle; // 存储循环次数
private List<TestResult> _testResults = []; // 存储测试结果
private int _successCount = 0; // 测试成功用例数量
private int _failCount = 0; // 测试失败用例数量
private TestResult _latestTestResult = new();
public event EventHandler OnResultUpdated; // 报告更新事件
public event EventHandler OnTestFinished; // 测试结束

private bool isStop = false;
private Dictionary<string, TestAttribute> _modifiedTestAttributes = new Dictionary<string, TestAttribute>();

public TestResult LatestTestResult
{
get { return _latestTestResult; }
set { _latestTestResult = value; }
}

public int SuccessCount
{
get { return _successCount; }
set { _successCount = value; }
}

public int FailCount
{
get { return _failCount; }
set { _failCount = value; }
}

/// <summary>运行所有测试</summary>
public void RunTests()
{
// 获取当前类的所有非公共实例方法
MethodInfo[] methods = GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance);
// 查找标记有[BeforeClass]特性的方法
MethodInfo beforeClassMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(BeforeClassAttribute)));
// 查找标记有[AfterClass]特性的方法
MethodInfo afterClassMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(AfterClassAttribute)));
// 获取所有标记有[Test]特性的方法,并按Grade排序
//var testMethods = methods
// .Where(m => Attribute.IsDefined(m, typeof(TestAttribute)))
// .Select(m => new { Method = m, Attr = m.GetCustomAttribute<TestAttribute>() })
// .OrderBy(t => t.Attr.Grade) // 直接按Grade排序
// .Select(t => t.Method)
// .ToArray();
var testMethods = methods
.Where(m => Attribute.IsDefined(m, typeof(TestAttribute)))
.Select(m => new { Method = m, Attr = GetTestAttribute(m) })
.OrderBy(t => t.Attr.Grade) // 直接按Grade排序
.Select(t => t.Method)
.ToArray();

// 如果存在[BeforeClass]方法,则调用它
beforeClassMethod?.Invoke(this, null);
int id = 0;
// 循环执行测试
for (int circleIndex = 1; circleIndex <= _circle; circleIndex++)
{
if (isStop)
break;
// 遍历每个测试方法
foreach (var testMethod in testMethods)
{
if (isStop)
break;
// 获取[Test]特性实例,以获取其属性
//var testAttr = testMethod.GetCustomAttribute<TestAttribute>();
var testAttr = GetTestAttribute(testMethod);
int count = testAttr.Count >= 0 ? testAttr.Count : 1;
string description = testAttr.Description;

// 根据[Test]特性中指定的次数执行测试
for (int countIndex = 1; countIndex <= count; countIndex++)
{
// 查找标记有[Before]特性的方法并执行
MethodInfo beforeMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(BeforeAttribute)));
beforeMethod?.Invoke(this, null);

TestResult result;
id++;
// 执行测试方法,并捕获结果(这里简化了,实际上应处理返回值和异常)
try
{
testMethod.Invoke(this, null);
SuccessCount++;
result = new TestResult {Id=id, Circle = circleIndex, Case = testMethod.Name, Description= description, Count = countIndex, Status = "Success", Message = "Success" };
}
catch (Exception ex)
{
FailCount++;
if(ex.InnerException != null)
result = new TestResult { Id = id, Circle = circleIndex, Case = testMethod.Name, Description = description, Count = countIndex, Status = "Fail", Message = ex.InnerException.Message };
else
result = new TestResult { Id = id, Circle = circleIndex, Case = testMethod.Name, Description = description, Count = countIndex, Status = "Fail", Message = ex.Message };
}
_testResults.Add(result);
LatestTestResult = result;
OnResultUpdated?.Invoke(this, EventArgs.Empty);

// 查找标记有[After]特性的方法并执行
MethodInfo afterMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(AfterAttribute)));
afterMethod?.Invoke(this, null);

}
}
}
// 查找标记有[AfterClass]特性的方法并执行
afterClassMethod?.Invoke(this, null);
OnTestFinished?.Invoke(this, EventArgs.Empty);
}

/// <summary>停止测试</summary>
public void Stop()
{
isStop = true;
}

public void RunFailedTests()
{

// 获取当前类的所有非公共实例方法
MethodInfo[] methods = GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance);
// 查找标记有[BeforeClass]特性的方法
MethodInfo beforeClassMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(BeforeClassAttribute)));
// 查找标记有[AfterClass]特性的方法
MethodInfo afterClassMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(AfterClassAttribute)));

// 如果存在[BeforeClass]方法,则调用它
beforeClassMethod?.Invoke(this, null);

// 遍历所有测试结果
for (int i = 0; i < _testResults.Count; i++)
{
var result = _testResults[i];
// 检查测试结果是否失败
if (result.Status == "Fail")
{
// 使用反射获取失败的测试方法
var testMethod = GetType().GetMethod(result.Case, BindingFlags.NonPublic | BindingFlags.Instance);
if (testMethod != null)
{
// 获取测试方法的TestAttribute属性
//var testAttr = testMethod.GetCustomAttribute<TestAttribute>();
var testAttr = GetTestAttribute(testMethod);
int count = testAttr.Count > 0 ? testAttr.Count : 1;
string description = testAttr.Description;

// 查找标记有[Before]特性的方法并执行
MethodInfo beforeMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(BeforeAttribute)));
beforeMethod?.Invoke(this, null);

TestResult rerunResult;
try
{
testMethod.Invoke(this, null);
SuccessCount++;
FailCount--;
rerunResult = new TestResult { Id = result.Id, Circle = result.Circle, Case = result.Case, Description = description, Count = result.Count, Status = "Success", Message = "Success" };
}
catch (Exception ex)
{
rerunResult = new TestResult { Id = result.Id, Circle = result.Circle, Case = result.Case, Description = description, Count = result.Count, Status = "Fail", Message = ex.Message };
}

// 修改原来的失败结果而不是新增结果
var index = _testResults.IndexOf(result);
if (index != -1)
{
_testResults[index] = rerunResult;
}
else
{
_testResults.Add(rerunResult);
}
LatestTestResult = rerunResult;
OnResultUpdated?.Invoke(this, EventArgs.Empty);

// 查找标记有[After]特性的方法并执行
MethodInfo afterMethod = methods.FirstOrDefault(m => Attribute.IsDefined(m, typeof(AfterAttribute)));
afterMethod?.Invoke(this, null);
}
}
}

// 查找标记有[AfterClass]特性的方法并执行
afterClassMethod?.Invoke(this, null);
OnTestFinished?.Invoke(this, EventArgs.Empty);
}

public void PrintTestResults()
{
foreach (var result in _testResults)
{
Log.Info($"Circle: {result.Circle}, Case: {result.Case}, Description: {result.Description}, Count: {result.Count}, Status: {result.Status}, Message: {result.Message}");
}
}

public List<TestResult> GetAllTestResults()
{
return _testResults;
}

public List<TestResult> GetSuccessTestResults()
{
List<TestResult> successTestResults = [];
for (int i = 0; i < _testResults.Count; i++)
{
var result = _testResults[i];
if (result.Status == "Success")
{
successTestResults.Add(result);
}
}
return successTestResults;
}

public List<TestResult> GetFailTestResults()
{
List<TestResult> failTestResults = [];
for (int i = 0; i < _testResults.Count; i++)
{
var result = _testResults[i];
if (result.Status == "Fail")
{
failTestResults.Add(result);
}
}
return failTestResults;
}

/// <summary>
/// 设置指定测试用例的 Count 次数
/// </summary>
/// <param name="methodName">测试用例的方法名称</param>
/// <param name="count">新的 Count 值</param>
public void SetCaseCount(string methodName, int count)
{
// 获取当前类的所有非公共实例方法
MethodInfo[] methods = GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Instance);

// 查找指定名称的测试方法
MethodInfo testMethod = methods.FirstOrDefault(m => m.Name == methodName) ?? throw new ArgumentException($"未找到名为 {methodName} 的测试方法。");

// 获取测试方法的 TestAttribute
var testAttr = testMethod.GetCustomAttribute<TestAttribute>() ?? throw new InvalidOperationException($"测试方法 {methodName} 未标记 [Test] 特性。");

// 创建一个新的 TestAttribute 实例并修改 Count 属性
var modifiedAttr = new TestAttribute(testAttr.Grade, count, testAttr.Description);
_modifiedTestAttributes[methodName] = modifiedAttr;
}

private TestAttribute GetTestAttribute(MethodInfo method)
{
if (_modifiedTestAttributes.TryGetValue(method.Name, out var modifiedAttr))
{
return modifiedAttr;
}
return method.GetCustomAttribute<TestAttribute>();
}
}
  • TestBase 是一个抽象类,接收一个 circle 参数,表示测试的循环次数。

  • _testResults包含存储测试结果成功和失败的测试用例计数,以及一个 OnResultUpdated 事件,用于在结果更新时触发通知。

  • RunTests运行所有测试

  • Stop停止测试。但是不会立刻停止,只有当前的测试用例结束后,才会停止

  • RunFailedTests运行失败的测试

  • GetAllTestResults获取所有测试结果

  • GetSuccessTestResults获取成功的测试结果

  • GetFailTestResults获取失败的测试结果

  • SetCaseCount设置指定测试用例的 Count 次数

简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class ExampleTests : TestBase
{
private BasicReportManager reportManager; // 用于管理测试报告的存储和更新

public ExampleTests(int circle=1) : base(circle)
{
reportManager = new BasicReportManager($"{DateFormat.Now(DateFormatType.DateTimeLink)}.db", "ExampleTest");
OnResultUpdated += ExampleTests_OnReportUpdated; // 订阅测试结果更新事件
}

private async void ExampleTests_OnReportUpdated(object sender, EventArgs e)
{
reportManager.InsertTestResult(LatestTestResult);
reportManager.UpdateTestInfo(SuccessCount, FailCount);
await Task.Delay(2000);
reportManager.UpdateReportPage();
}

[BeforeClass]
private void BeforeClass()
{
reportManager.TestStart();
}

[Before]
private void Before()
{
Log.Info("before method");
}

[Test(grade:1, count: 50, description:"判断偶数")]
private void TestMethod1()
{
Thread.Sleep(200);
int randomNumber = new Random().Next(1, 11);
if (randomNumber % 2 == 0)
throw new Exception("这是一个偶数");
}

[Test(grade: 1, count: 50, description: "判断奇数")]
private void TestMethod2()
{
Thread.Sleep(200);
int randomNumber = new Random().Next(1, 11);
if (randomNumber % 2 != 0)
throw new Exception("这是一个奇数");
}

[After]
private void After()
{
Log.Info("after method");
}

[AfterClass]
private void AfterClass()
{
reportManager.TestEnd();
PrintTestResults();
}
}

BasicReportManager是一个自定义用来管理测试报告的类,可自由发挥。

在构造方法里,传入了测试次数circle,通过[BeforeClass][Before][Test][After][AfterClass]来标记测试的不同阶段和测试方法。其中[Test]可以设置 gradecountdescription 属性。

测试执行

1
2
ExampleTests exampleTests = new();
exampleTests.RunTests();

测试结果

BasicReportManager自动生成