qdjjx 3 лет назад
Родитель
Сommit
5d21d2ffd4

+ 430
- 0
专题/后端/测试/01测试开始篇.md Просмотреть файл

@@ -0,0 +1,430 @@
1
+# .NET中的测试
2
+
3
+测试类型
4
+
5
+- 单元测试(`Unit tests`):测试单个组件或方法,与数据库、文件、外界交互无关
6
+- 集成测试(Integration tests):测试多个组件或方法,与数据库、文件、外界交互有关
7
+- 压力测试(Load tests):测试系统对压力的承受程度,比如并发用户、并发请求等
8
+
9
+
10
+
11
+测试框架
12
+
13
+- `xUnit`:来自`NUnit v2`的作者,包括了最新的测试技术和思想。属于`.NET基金会`下的项目。
14
+- `NUnit`:从`JUnit`演变而来,最新的版本增加了很多新功能。属于`.NET基金会`下的项目。
15
+- `MSTest`:微软的测试框架,支持在`.NET CLI`和`Visual Studio`中的扩展
16
+
17
+
18
+
19
+`.NET CLI`
20
+
21
+`.NET CLI`集成了`IDE`下的大部分功能,通过使用`dotnet test`命令运行。可以被用到持续集成交付管道中,也可以被用在测试自动化任务中。
22
+
23
+
24
+
25
+`IDE`
26
+
27
+相比`.NET CLI`有更多的功能,比如`Live Unit Testing`是`IDE`独有的测试功能。
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
+测试覆盖率`Code coverage`
53
+
54
+测试覆盖率不见得越大越好,只是表明有多少代码被测试了,需要在实际项目中把握测试覆盖率的度。
55
+
56
+
57
+
58
+## 理解`Fake`, `Mock`, `Stub`
59
+
60
+- `Fake`:不确定是`Mock`还是`Stub`的时候用`Fake`
61
+- `Mock`:直接影响到测试结果就用`Mock`
62
+- `Stub`: 只是某个依赖的替代,不直接影响到测试结果
63
+
64
+
65
+
66
+不好的
67
+
68
+```
69
+var mockOrder = new MockOrder();
70
+var purchase = new Purchase(mockOrder);
71
+purchase.ValidateOrders();
72
+Assert.True(purchase.CanBeShipped);//测试结果与MockOrder没有直接关系
73
+```
74
+
75
+好的
76
+
77
+```
78
+var stubOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub
79
+var purchase = new Purchase(stubOrder);
80
+purchase.ValdiateOrders();
81
+Assert.True(purchase.CanBeShipped);//测试结果与stubOrder没有关系
82
+```
83
+
84
+好的
85
+
86
+```
87
+var mockOrder = new FakeOrder();//使用Fake可以转换成Mock或Stub
88
+var purchase = new Purchase(mockOrder);
89
+purchase.ValidateOrders();
90
+Assert.True(mockOrder.Valdiated);//测试结果与mockOrder有直接关系
91
+```
92
+
93
+> 命名。好的命名说明了测试意图。通常是:`被测方法名称_场景或条件_期待结果`
94
+
95
+不好的
96
+
97
+```
98
+[Fact]
99
+public void Test_Single(){}
100
+```
101
+
102
+好的
103
+
104
+```
105
+[Fact]
106
+public void Add_SingleNumber_ReturnsSameNumber(){}
107
+```
108
+
109
+## 遵循`AAA`原则
110
+
111
+不好的
112
+
113
+```
114
+[Fact]
115
+public void Add_EmptyString_ReturnsZero()
116
+{
117
+	//Arrange
118
+	var stringCalculator = new StringCalculator();
119
+	
120
+	//Assert
121
+	Assert.Equal(0, stringCalculator.Add(""));
122
+}
123
+```
124
+
125
+好的
126
+
127
+```
128
+[Fact]
129
+public void Add_EmptyString_ReturnsZero()
130
+{
131
+	//Arrange
132
+	var stringCalculator = new StringCalculator();
133
+	
134
+	//Act
135
+	var actual = stringCalculator.Add("");
136
+	
137
+	//Assert
138
+	Assert.Equal(0, actual);
139
+}
140
+```
141
+
142
+## 保持输入简单原则
143
+
144
+不好的
145
+
146
+```
147
+[Fact]
148
+public void Add_SingleNumber_ReturnsSameNumber()
149
+{
150
+    var stringCalculator = new StringCalculator();
151
+
152
+    var actual = stringCalculator.Add("42");
153
+
154
+    Assert.Equal(42, actual);
155
+}
156
+```
157
+
158
+好的
159
+
160
+```
161
+[Fact]
162
+public void Add_SingleNumber_ReturnsSameNumber()
163
+{
164
+    var stringCalculator = new StringCalculator();
165
+
166
+    var actual = stringCalculator.Add("0");
167
+
168
+    Assert.Equal(0, actual);
169
+}
170
+```
171
+
172
+## 避免使用系统保留字
173
+
174
+不好的
175
+
176
+```
177
+[Fact]
178
+public void Add_BigNumber_ThrowsException()
179
+{
180
+    var stringCalculator = new StringCalculator();
181
+
182
+    Action actual = () => stringCalculator.Add("1001");
183
+
184
+    Assert.Throws<OverflowException>(actual);
185
+}
186
+```
187
+
188
+好的
189
+
190
+```
191
+[Fact]
192
+void Add_MaximumSumResult_ThrowsOverflowException()
193
+{
194
+    var stringCalculator = new StringCalculator();
195
+    const string MAXIMUM_RESULT = "1001";//赋值给常量
196
+
197
+    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);
198
+
199
+    Assert.Throws<OverflowException>(actual);
200
+}
201
+```
202
+
203
+## 避免逻辑
204
+
205
+不好的
206
+
207
+```
208
+[Fact]
209
+public void Add_MultipleNumbers_ReturnsCorrectResults()
210
+{
211
+    var stringCalculator = new StringCalculator();
212
+    var expected = 0;
213
+    var testCases = new[]
214
+    {
215
+        "0,0,0",
216
+        "0,1,2",
217
+        "1,2,3"
218
+    };
219
+
220
+    foreach (var test in testCases)
221
+    {
222
+        Assert.Equal(expected, stringCalculator.Add(test));
223
+        expected += 3;
224
+    }
225
+}
226
+```
227
+
228
+好的
229
+
230
+```
231
+[Theory]
232
+[InlineData("0,0,0", 0)]
233
+[InlineData("0,1,2", 3)]
234
+[InlineData("1,2,3", 6)]
235
+public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
236
+{
237
+    var stringCalculator = new StringCalculator();
238
+
239
+    var actual = stringCalculator.Add(input);
240
+
241
+    Assert.Equal(expected, actual);
242
+}
243
+```
244
+
245
+## 帮助方法替代`setup`和`teadown`
246
+
247
+不好的
248
+
249
+```
250
+private readonly StringCalculator stringCalculator;
251
+public StringCalculatorTests()
252
+{
253
+    stringCalculator = new StringCalculator();
254
+}
255
+```
256
+
257
+```
258
+[Fact]
259
+public void Add_TwoNumbers_ReturnsSumOfNumbers()
260
+{
261
+    var result = stringCalculator.Add("0,1");
262
+
263
+    Assert.Equal(1, result);
264
+}
265
+```
266
+
267
+好的
268
+
269
+```
270
+[Fact]
271
+public void Add_TwoNumbers_ReturnsSumOfNumbers()
272
+{
273
+    var stringCalculator = CreateDefaultStringCalculator();
274
+
275
+    var actual = stringCalculator.Add("0,1");
276
+
277
+    Assert.Equal(1, actual);
278
+}
279
+
280
+private StringCalculator CreateDefaultStringCalculator()
281
+{
282
+    return new StringCalculator();
283
+}
284
+```
285
+
286
+## 避免一个测试方法中出现多个推断
287
+
288
+不好的
289
+
290
+```
291
+[Fact]
292
+public void Add_EdgeCases_ThrowsArgumentExceptions()
293
+{
294
+    Assert.Throws<ArgumentException>(() => stringCalculator.Add(null));
295
+    Assert.Throws<ArgumentException>(() => stringCalculator.Add("a"));
296
+}
297
+```
298
+
299
+好的
300
+
301
+```
302
+[Theory]
303
+[InlineData(null)]
304
+[InlineData("a")]
305
+public void Add_InputNullOrAlphabetic_ThrowsArgumentException(string input)
306
+{
307
+    var stringCalculator = new StringCalculator();
308
+
309
+    Action actual = () => stringCalculator.Add(input);
310
+
311
+    Assert.Throws<ArgumentException>(actual);
312
+}
313
+```
314
+
315
+## 测试公共方法而不是私有方法
316
+
317
+```
318
+public string ParseLogLine(string input)
319
+{
320
+    var sanitizedInput = TrimInput(input);
321
+    return sanitizedInput;
322
+}
323
+
324
+private string TrimInput(string input)
325
+{
326
+    return input.Trim();
327
+}
328
+
329
+[Fact]
330
+public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
331
+{
332
+    var parser = new Parser();
333
+
334
+    var result = parser.ParseLogLine(" a ");
335
+
336
+    Assert.Equals("a", result);
337
+}
338
+```
339
+
340
+## 使用接口获取静态值
341
+
342
+有一个和日期相关的方法
343
+
344
+```
345
+public int GetDiscountedPrice(int price)
346
+{
347
+    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
348
+    {
349
+        return price / 2;
350
+    }
351
+    else
352
+    {
353
+        return price;
354
+    }
355
+}
356
+```
357
+
358
+不好的
359
+
360
+```
361
+public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
362
+{
363
+    var priceCalculator = new PriceCalculator();
364
+
365
+    var actual = priceCalculator.GetDiscountedPrice(2);
366
+
367
+    Assert.Equals(2, actual)
368
+}
369
+
370
+public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
371
+{
372
+    var priceCalculator = new PriceCalculator();
373
+
374
+    var actual = priceCalculator.GetDiscountedPrice(2);
375
+
376
+    Assert.Equals(1, actual);
377
+}
378
+```
379
+
380
+不管哪一天,总一个通不过。
381
+
382
+好的。提炼出一个获取日期的接口。
383
+
384
+```
385
+public interface IDateTimeProvider
386
+{
387
+    DayOfWeek DayOfWeek();
388
+}
389
+```
390
+
391
+```
392
+public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
393
+{
394
+    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
395
+    {
396
+        return price / 2;
397
+    }
398
+    else
399
+    {
400
+        return price;
401
+    }
402
+}
403
+```
404
+
405
+```
406
+public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
407
+{
408
+    var priceCalculator = new PriceCalculator();
409
+    var dateTimeProviderStub = new Mock<IDateTimeProvider>();//模拟接口
410
+    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);//模拟接口实现
411
+
412
+    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
413
+
414
+    Assert.Equals(2, actual);
415
+}
416
+
417
+public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
418
+{
419
+    var priceCalculator = new PriceCalculator();
420
+    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
421
+    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);
422
+
423
+    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);
424
+
425
+    Assert.Equals(1, actual);
426
+}
427
+```
428
+
429
+
430
+

+ 83
- 0
专题/后端/测试/02使用xUnit进行TDD测试驱动开发.md Просмотреть файл

@@ -0,0 +1,83 @@
1
+使用`TDD`方式。
2
+
3
+```
4
+    public class MyPrimeService
5
+    {
6
+        public bool IsPrime(int num)
7
+        {
8
+            throw new NotImplementedException("not implemented");
9
+        }
10
+    }
11
+```
12
+
13
+单元测试
14
+
15
+```
16
+        [Fact]
17
+        public void IsPrime_InputIs1_ReturnFalse()
18
+        {
19
+            //Arrange
20
+            var primeService = new MyPrimeService();
21
+
22
+            //Act
23
+            bool result = primeService.IsPrime(1);
24
+
25
+            //Assert
26
+            Assert.False(result, "1 should not be prime");
27
+        }
28
+```
29
+
30
+修改代码让单元测试通过。
31
+
32
+```
33
+        public bool IsPrime(int num)
34
+        {
35
+            if(num==1)
36
+            {
37
+                return false;
38
+            }
39
+            throw new NotImplementedException("not implemented");
40
+        }
41
+```
42
+
43
+目前单元测试输入条件有限,输入更多条件。
44
+
45
+```
46
+        [Theory]
47
+        [InlineData(-1)]
48
+        [InlineData(0)]
49
+        [InlineData(1)]
50
+        public void IsPrime_ValueLessThanTwo_ReturnFalse(int value)
51
+        {
52
+            //Arrange
53
+            var primeService = new PrimeService.MyPrimeService();
54
+
55
+            //Act
56
+            var restul = primeService.IsPrime(value);
57
+
58
+            //Assert
59
+            Assert.False(restul, $"{value} should not be prime");
60
+        }
61
+```
62
+
63
+有两个不通过
64
+
65
+```
66
+失败!  - Failed:     2, Passed:     1, Skipped:     0, Total:     3, Duration: 4 ms
67
+```
68
+
69
+其实所有都不应该通过。修改代码:
70
+
71
+```
72
+        public bool IsPrime(int num)
73
+        {
74
+            if(num < 2)
75
+            {
76
+                return false;
77
+            }
78
+            throw new NotImplementedException("not implemented");
79
+        }
80
+```
81
+
82
+
83
+

+ 502
- 0
专题/后端/测试/03CleanArchitecture下的CleanTesting.md Просмотреть файл

@@ -0,0 +1,502 @@
1
+# 用到的组件
2
+
3
+
4
+
5
+本篇涉及的测试形式:
6
+
7
+- `Subcutaneous`测试:最接近`UI`层的测试,聚焦在输入和输出,非常适合逻辑和UI分离的场景,这样就避免进行UI测试,容易写也容易维护
8
+- `Integration`集成测试:
9
+- `Unit`单元测试:
10
+
11
+
12
+
13
+## `FluentAssertions`组件
14
+
15
+- 支持非常自然地规定自动测试的结果
16
+- 更好的可读性
17
+- 更好的测试失败说明
18
+- 避免调试
19
+- 更有生产力,更简单
20
+
21
+
22
+
23
+## `Moq`组件
24
+
25
+- 为`.NET`而来
26
+- 流行并且友好
27
+- 支持`Mock`类和接口
28
+- 强类型,这样可以避免和系统关键字冲突
29
+- 简单易用
30
+
31
+
32
+
33
+## `EF Core InMemory`组件
34
+
35
+- 内置数据库引擎
36
+- 内存数据库
37
+- 适用于测试场景
38
+- 轻量级无依赖
39
+
40
+
41
+
42
+## `Respawn`组件
43
+
44
+- 为集成测试准备的数据库智能清洗
45
+- 避免删除数据或回滚事务
46
+- 把数据库恢复到一个干净的某个节点
47
+- 忽略表和`Schema`,比如忽略`_EFMigrationsHistory`
48
+- 支持`SQL Server`, `Postgres`,`MySQL`
49
+
50
+
51
+
52
+# 界面
53
+
54
+## 列表
55
+
56
+![t1](F:\SourceCodes\DDWiki\专题\后端\测试\t1.png)
57
+
58
+## 删除和更新
59
+
60
+![t2](F:\SourceCodes\DDWiki\专题\后端\测试\t2.png)
61
+
62
+# 解决方案组件依赖
63
+
64
+- `.NET Core Template Package`
65
+- `ASP.NET Core 3.1`
66
+- `Entity Framework Core 3.1`
67
+- `ASP.NET Core Identity 3.1`
68
+- `Angular 9`
69
+
70
+# 文件结构
71
+
72
+- `Application`层
73
+- `Domain`层
74
+- `Infrastructure`层
75
+- `WebUI`层
76
+-  集成测试层
77
+
78
+
79
+
80
+# `WebUI`层
81
+
82
+`TodoListsController`
83
+
84
+```
85
+[Authorize]
86
+public class TodoListsController : ApiController
87
+{
88
+	[HttpGet]
89
+	public async Task<IActionResult<TodoVm>> Get()
90
+	{
91
+		return await Mediator.Send(new GetTodosQuery());
92
+	}
93
+	
94
+	[HttpGet("{id}")]
95
+	public async Task<FileResult> Get(int id)
96
+	{
97
+		var vm = await Mediator.Send(new ExportTodosQuery {ListId=id});
98
+		return File(vm.Content, vm.ContentType, vm.FileName);
99
+	}
100
+	
101
+	[HttpPost]
102
+	public async Task<IActionResult<int>> Create(CreateTodoListCommand command)
103
+	{
104
+		return await Mediator.Send(command);
105
+	}
106
+}
107
+
108
+
109
+```
110
+
111
+# `Application`层
112
+
113
+`GetTodosQuery.cs`
114
+
115
+```
116
+public class GetTodosQuery : IRequest<TodosVm>
117
+{
118
+
119
+}
120
+
121
+public class GetTodosQueryHandler : IRequestHandler<GetTodosQuery, TodosVm>
122
+{
123
+	private readonly IApplicationDbContext _context;
124
+	private readonly IMapper _mapper;
125
+	
126
+	public GetTodosQueryHandler(IApplicationDbContext context, IMapper mapper)
127
+	{
128
+		_context = context;
129
+		_mapper = mapper;
130
+	}
131
+	
132
+	public async Task<TodosVm> Handler(GetTodosQuery request, CancellationToken cancellationToken)
133
+	{
134
+		return new TodosVm{
135
+			PriorityLevels = Enum.GetValues(typeof(PriorityLevel))
136
+				.Cast<PriorityLevel>()
137
+				.Select(p => new PriorityLevelDto{Value=(int)p, Name=p.ToString()})
138
+				.ToList,
139
+			Lists = await _context.TodosLists
140
+				.ProjectTo<TodoListDto>(_mapper.ConfigurationProvider)
141
+				.OrdreBy(t => t.Title)
142
+				.ToListAsync(cacnellationToken);
143
+		};
144
+	}
145
+}
146
+```
147
+
148
+在`MediatR`的请求管道中做验证。
149
+
150
+```
151
+public class RequestValidationBehavior<TRequest, TResponse> : IPiplelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
152
+{
153
+	private readonly IENumerable<IValidator<TRequest>> _validators;
154
+	
155
+	public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
156
+	{
157
+		_validators = validators;
158
+	}
159
+	
160
+	public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
161
+	{
162
+		if(_validators.Any())
163
+		{
164
+			var context = new ValidationContext(request);
165
+			
166
+			var failures = _validators
167
+				.Select(v => v.Validate(context))
168
+				.SelectMany(result => result.Errors)
169
+				.Whre(f => f != null)
170
+				.ToList();
171
+				
172
+			if(failures.Count != 0)
173
+			{
174
+				throw new ValidationException(failures);
175
+			}
176
+		}
177
+		return next();
178
+	}
179
+}
180
+```
181
+
182
+在`MediatR`的请求管道中处理异常。
183
+
184
+```
185
+public class UnhandledExceptionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
186
+{
187
+	private readonly ILogger<TRequest> _logger;
188
+	
189
+	public UnhandledExceptionBehaviour(ILogger<TRequest> logger)
190
+	{
191
+		_logger = logger;
192
+	}
193
+	
194
+	public async Task<TResponse> Handle(TRequest request, CancellationTOken canellationToken, RequestHandlerDelegate<TResponse> next)
195
+	{
196
+		try	
197
+		{
198
+			return await next();
199
+		}
200
+		catch(Exception ex)
201
+		{
202
+			var requestName = typeof(TRequest).Name;
203
+			_logger.LogError();
204
+			throw;
205
+		}
206
+	}
207
+}
208
+```
209
+
210
+# 集成测试层
211
+
212
+引用`WebUI`层
213
+
214
+```
215
+dotnet add package FluentAssertions
216
+dotnet add package Moq
217
+dotnet add package Respawn
218
+```
219
+
220
+`GetTodosTests.cs`
221
+
222
+```
223
+
224
+using static Testing;
225
+public class GetTodosTests : TestBase //每次运行测试让数据库回到干净的状态
226
+{
227
+	[Test]
228
+	public async Task ShouldReturnAllListsAndAssociatedItems()
229
+	{
230
+		//Arrange
231
+		await AddAsync(new TodoList{
232
+			Title = "",
233
+			Items = {
234
+				new TodoItem {Title="",Done=true},
235
+				new TodoItem {Title="",Done=true},
236
+				new TodoItem {Title="",Done=true},
237
+				new TodoItem {Title=""},
238
+				new TodoItem {Title=""},
239
+				new TodoItem {Title=""}
240
+			}
241
+		});
242
+		var query = new GetTodosQuery();
243
+		
244
+		//Act
245
+		TodosVm result = await SendAsync(query);
246
+		
247
+		//Assert
248
+		result.Should().NotBeNull();
249
+		result.List.Should().HaveCount(1);
250
+		result.Lists.First().Items.Should().HaveCount();
251
+	}
252
+}
253
+```
254
+
255
+`CreateTodoListTests.cs`
256
+
257
+```
258
+
259
+using static Testing;
260
+public class CreateTodListTests : TestBase//每次运行测试让数据库回到干净的状态
261
+{
262
+	[Test]
263
+	public void ShouldRequredMinimumFileds()
264
+	{
265
+		var command = new CrateTodoListCommand();
266
+		FluenActions.Invoking(()=> SendAsync(command))
267
+			.Should().Throw<ValidaitonException>();
268
+	}
269
+	
270
+	[Test]
271
+	public async Task ShouldRequiredUniqueTitle()
272
+	{
273
+		await SendAsync(new CreateTodoListCommand{
274
+			Title = ""
275
+		});
276
+		
277
+		var command = new CreateTodoListCommand
278
+		{
279
+			Title = ""
280
+		};
281
+		
282
+		
283
+		FluenActions.Invoking(() => SendAsync(comamnd))
284
+			.Should().Throw<ValidationException>();
285
+	}
286
+	
287
+	[Test]
288
+	public async Task ShouldCreateTodoList()
289
+	{
290
+	
291
+		var useId = await RunAsDefaultUserAsync();
292
+		var command = new CreateTodoListCommand
293
+		{
294
+			Title = ""
295
+		};
296
+		
297
+		var listeId = await SendAsync(command);
298
+		
299
+		var list = await FindAsync<TodoList>(listId);
300
+		
301
+		list.Should().NotBuNull();
302
+		list.Title.Should().Be(command.Title);
303
+		list.Created.Should().BeCloseTo(DateTime.Now, 10000);
304
+		list.CreatedBy.Should().Be(userId);
305
+	}
306
+}
307
+```
308
+
309
+`UpdateDotListTests.cs`
310
+
311
+```
312
+using static Testing;
313
+public class UpdateTodoListTests : TestBase
314
+{
315
+	[Test]
316
+	public void ShouldRequireValidTodoListId()
317
+	{
318
+		var command = new UpdateTodoListCommand
319
+		{
320
+			Id = 99,
321
+			Title = "New Title"
322
+		};
323
+		
324
+		FluenActions.Invoking(() => SendAsync(command)).Should().Throw<NotFoundException>();
325
+	}
326
+	
327
+	[Test]
328
+	public async Task ShouldRequireUniqueTitle()
329
+	{
330
+		var listId = await SendAsync(new CreateTodoListCommand{Title=""});
331
+		
332
+		await SendAsync(new CreateTodoListCommand={Title=""});
333
+		var command = new UpdateTodoListCommand
334
+		{
335
+			Id = listId,
336
+			Title = ""
337
+		};
338
+		
339
+		FluentActions.Invoking(()=>
340
+			SendAsync(command)).Should().Throw<ValidationException>().Where(ex => ex.Errors.ContainsKey("Title"))
341
+			.And.Erros["Title"].Should().Contain("The specified title is already exists.");
342
+	}
343
+	
344
+	[Test]
345
+	public async Task ShouldUpdateTodoList()
346
+	{
347
+		var userId = await RunAsDefaultUserAsync();
348
+		var listId = await SendAsync(new CreateTodoListCommand{
349
+			Title = ""
350
+		});
351
+		
352
+		var command = new UpdateTodoListCommand
353
+		{
354
+			Id = listId,
355
+			Title= ""
356
+		};
357
+		await SendAsync(command);
358
+		var list =await FindAsync<TodoList>(listId);
359
+		
360
+		list.Title.Should().Be(command.Title);
361
+		list.LastModifiedBy.Should().NotBeNull();
362
+		list.LastModifiedBy.Should().Be(userId);
363
+		list.LastModified.Should().NotBeNull();
364
+		list.LstModified.Should().BeCloaseTo(DateTime.Now, 1000);
365
+	}
366
+}
367
+```
368
+
369
+`Testing.cs`
370
+
371
+```
372
+[SetUpFixture]
373
+public class Testing
374
+{
375
+
376
+	private static IConfiguration _configuration;//配置
377
+	private static IServiceScopeFactory _scopeFactory;//域
378
+	private static Checkpoint _checkpoint;//Respawn
379
+	private static string _currentUserId;
380
+
381
+	[OneTimeSetUp]
382
+	public void RunBeforeAnyTests()
383
+	{
384
+		var buidler = new ConfigurationBuilder()
385
+			.SetBasePath(Directory.GetCurrentDirectory)
386
+			.AddJsonFile("appsettings.json", true, true)
387
+			.AddEnvironmentVariables();
388
+			
389
+		_configuration = builder.Build();
390
+		
391
+		var services = new ServiceCollection();//看作是容器
392
+		var startup = new Startup(_configuration);//Startup.cs需要IConfiguration
393
+		
394
+		//ServiceCollection需要宿主
395
+		services.AddSignleton(Mock.Of<IWebHostEnvironment>(w =>
396
+			w.ApplcationName == "CleanTesting.WebUI" &&
397
+			w.EnvirnmentName == "Development"));
398
+		
399
+		startup.ConfigureServices(services);
400
+		
401
+		//Replace service registration for ICurrentUserService
402
+		//Remove existing registration
403
+		var currentUserServiceDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(ICurrentUserService));
404
+		service.Remove(currentUserServiceDescriptor);
405
+		
406
+		//Register testing version
407
+		services.AddTransient(provider => 
408
+			Mock.Of<ICurrentUserService>(s => s.UserId == _currentUserId));
409
+		
410
+		_scopeFactory = services.BuilderServiceProvider().GetService<IServiceScopeFactory>();
411
+		
412
+		_checkpoint = new Checkpoint{
413
+			TablesToIgnore  = new [] {"__EFMigrationsHistory"}
414
+		};
415
+	}
416
+	
417
+	public static async Task<string> RunAsDefaultUserAsync()
418
+	{
419
+		return await RundAsUserAsync("","");
420
+		_currentUserId = null;
421
+	}
422
+	
423
+	public static async Task<string> RunAsUserAsync(string userName, string password)
424
+	{
425
+		using var scope = _scopeFactory.CreateScope();
426
+		var userManager = scope.ServiceProvider.GetService<UserManager<ApplicaitonUser>>();
427
+		
428
+		var user = new ApplicationUser{UserName=userName, Email = userName};
429
+		var result = await  userManager.CreateAsync(user, password);
430
+		_currentUserId = user.Id;
431
+		return _currentUserId;
432
+	}
433
+	
434
+	public static async Task ResetState()
435
+	{
436
+		await _checkpoint.Reset(_configuration.GetConnectionString("DefaultConnection"));
437
+	}
438
+	
439
+	public static async Task AddAsync<TEntity>(TEntity entity) where TEntity : class
440
+	{
441
+		using var scope = _scopeFactory.CreateScope();
442
+		var context = scope.ServiceProvider.GetService<ApplicationDbContext>();
443
+		context.Add(entity);
444
+		await context.SaveChangesAsync();
445
+	}
446
+	
447
+	public static async Task<TResponse> SendAsync<TResponse>(IRequest<TResponse> request)
448
+	{
449
+		using var scope = _scopeFacotry.CreateScope();
450
+		var mediator = scope.ServiceProvider.GetService<IMediator>();
451
+		return await mediator.Send(request);
452
+	}
453
+	
454
+	public static async Task<TEntity> FindAsync<TEntity>(int id)
455
+	{
456
+		using var scope = _scopeFactory.CreateScope();
457
+		var context = scope.ServiceProvider.GetService<ApplicationContext>();
458
+		return await context.FindAsync<TEntity>(id);
459
+	}
460
+}
461
+```
462
+
463
+`appSetting.json`
464
+
465
+```
466
+{
467
+	"UseInMemoryDatabase":false,
468
+	"ConnectionStrings":{
469
+		"DefaultConnection" : "Server=(localdb)\\mssqllocaldb;Database=CleanTestingDb;Truested_Connection=True;MultipleActiveRestultSet"
470
+	},
471
+	"IdentityServer":{
472
+		"Clients":{
473
+			"CleanTesting.WebUI": {
474
+				"Profile": "IdentityServerSPA"
475
+			}
476
+		},
477
+		"Key":{
478
+			"Type" : "Development"
479
+		}
480
+	}
481
+}
482
+```
483
+
484
+`TestBase.cs`
485
+
486
+```
487
+using static Testing;
488
+public calss TestBase
489
+{
490
+	[SetUp]
491
+	public async Task SetUp()
492
+	{
493
+		await ResetState();
494
+	}
495
+}
496
+```
497
+
498
+# 资源
499
+
500
+- `Clean Testing Sample Code`: `https://github.com/jasontaylordev/cleantesting`
501
+- `Rules to Better Unit Tests`: `https://rules.ssw.com.au/rules-to-better-unit-tests`(翻墙)
502
+- `Fixie Demo`: `https://github.com/fixie/fixie.demo`

Двоичные данные
专题/后端/测试/t1.png Просмотреть файл


Двоичные данные
专题/后端/测试/t2.png Просмотреть файл


Загрузка…
Отмена
Сохранить