Browse Source

specification pattern

master
qdjjx 4 years ago
parent
commit
b4a1acdb67

专题/后端/2020.2.13领域驱动实战.md → 专题/后端/DDD/2020.2.13领域驱动实战.md View File

1
 ---
1
 ---
2
+
2
 typora-root-url: ./
3
 typora-root-url: ./
3
 ---
4
 ---
4
 
5
 

+ 61
- 0
专题/后端/DDD/2、替代throw的组件.md View File

1
+往往在架构中`throw`随处可见。
2
+
3
+```
4
+throw new ArgumentException($"Input {parameterName} was not in required format", parameterName);
5
+```
6
+
7
+`Ardalis.GuardClauses`用来解决这个问题。
8
+
9
+```
10
+<ItemGroup>
11
+	<PackageReference Include="Ardalis.GuardClauses" Version="3.0.1" />
12
+</ItemGroup>
13
+```
14
+
15
+用法
16
+
17
+```
18
+    public class Order
19
+    {
20
+        private string _name;
21
+        private int _quantity;
22
+        private long _max;
23
+        private decimal _unitPrice;
24
+        private DateTime _dateCreated;
25
+
26
+        public Order(string name, int quantity, long max, decimal unitPrice, DateTime dateCreated)
27
+        {
28
+            _name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
29
+            _quantity = Guard.Against.NegativeOrZero(quantity, nameof(quantity));
30
+            _max = Guard.Against.Zero(max, nameof(max));
31
+            _unitPrice = Guard.Against.Negative(unitPrice, nameof(unitPrice));
32
+            _dateCreated = Guard.Against.OutOfSQLDateRange(dateCreated, nameof(dateCreated));
33
+        }
34
+    }
35
+```
36
+
37
+通过`IGuardClause`实现扩展
38
+
39
+```
40
+namespace Ardalis.GuardClauses
41
+{
42
+    public static class Extensions
43
+    {
44
+        public static string Foo(this IGuardClause guardClause, string input, string parameterName)
45
+        {
46
+            if(input?.ToLower() == "foo")
47
+            {
48
+                throw new ArgumentException("Should not have been foo", parameterName);
49
+            }
50
+            return input;
51
+        }
52
+    }
53
+}
54
+```
55
+
56
+于是
57
+
58
+```
59
+_name = Guard.Against.Foo(name, nameof(name));
60
+```
61
+

+ 230
- 0
专题/后端/DDD/3、Specification Patter规格模式.md View File

1
+# 从一段代码说起
2
+
3
+通常在`Application`层的`IRequestHandler`中涉及到查询、排序、分页会有一大段代码。
4
+
5
+```
6
+public class SomeRequest : IRequest<DDResponseWrapper<SomeRequestDto>>
7
+{
8
+	//是否加载一对多关系
9
+	public bool LoadChildren {get;set;}
10
+	public bool IsPagingEnabled{get;set;}
11
+	public int Page{get;set;}
12
+	public int PageSize{get;set;}
13
+	...
14
+}
15
+
16
+public class SomeRequestDto
17
+{
18
+
19
+}
20
+
21
+public class SomeRequestHandler : IRequestHandler<SomeRequest, DDResponseWrapper<SomeRequestDto>>
22
+{
23
+	private readonly IBreakerRepo _breakerRepo;
24
+	
25
+	public SomeRequestHandler(IBreakerRepo breakerRepo)
26
+	{
27
+		_breakerRepo = breakerRepo;
28
+	}
29
+	
30
+	public asyn Task<DDResponseWrapper<SomeRequestDto>> Handle(SomeRequest request, CancellationTolen canellationTolen)
31
+	{
32
+		var breakers = _breakerRepo.GetAll();
33
+		
34
+		breakers = breakers.OrderBy(t=>t.Name);
35
+		
36
+		if(request.LoadChildren)
37
+		{
38
+			breakers = breakers.Include(t => t.BreakerData);
39
+		}
40
+		
41
+		if(request.IsPagingEnablled)
42
+		{
43
+			breakers = breakers.Skip(request.Page).Take(request.PageSize);
44
+		}
45
+		
46
+		if(!string.IsNullOrEmpty(request.Name))
47
+		{
48
+			breakers = breakers.Where(t=>t.Name.Contains(request.Name));
49
+		}
50
+		
51
+		......
52
+	}
53
+}
54
+```
55
+
56
+> 是否可以把这部分逻辑放到基础设施层、或者领域层呢?然后通过调用`var breakers =await  _breakerRepo.ListAsync(request)`替换以上逻辑?在`DDD`中,有一种`Specification Patter`模式特别适合这种场景,可以把相关逻辑封装在领域层,这样根据模块化,也更方便单元测试。
57
+
58
+```
59
+//在领域层定义Specification<T>
60
+public class CustomerSpec : Specification<Customer>
61
+{
62
+	public CustomerSpec(CustomerFilter filter)
63
+	{
64
+		...
65
+	}
66
+}
67
+
68
+//应用
69
+var spec = new CustomerSpec();
70
+var breakers = await _breakerRepo.ListAsync(spec);
71
+```
72
+
73
+`Ardalis.Specification`遵循了`Specification Pattern`,很好地解决了上述问题。
74
+
75
+```
76
+<PackageReference Include="Ardalis.Specification" Version="4.2.0" />
77
+<PackageReference Include="Ardalis.Specification.EntityFrameworkCore" Version="4.2.0" />
78
+```
79
+
80
+# 领域层
81
+
82
+一对多关系
83
+
84
+```
85
+    public class Customer : IAggregateRoot
86
+    {
87
+        public int Id { get; private set; }
88
+        public string Name { get; private set; }
89
+        public string Email { get; private set; }
90
+        public string Address { get; private set; }
91
+
92
+        public IEnumerable<Store> Stores => _stores.AsEnumerable();
93
+        private readonly List<Store> _stores = new List<Store>();
94
+
95
+        public Customer(string name, string email, string address)
96
+        {
97
+            Guard.Against.NullOrEmpty(name, nameof(name));
98
+            Guard.Against.NullOrEmpty(email, nameof(email));
99
+
100
+            this.Name = name;
101
+            this.Email = email;
102
+            this.Address = address;
103
+        }
104
+        ......
105
+    }
106
+    
107
+    public class Store
108
+    {
109
+        public int Id { get; private set; }
110
+        public string Name { get; private set; }
111
+        public string Address { get; private set; }
112
+
113
+        public int CustomerId { get; private set; }
114
+
115
+        public Store(string name, string address)
116
+        {
117
+            Guard.Against.NullOrEmpty(name, nameof(name));
118
+
119
+            this.Name = name;
120
+            this.Address = address;
121
+        }
122
+    }
123
+```
124
+
125
+查询条件
126
+
127
+```
128
+    public class BaseFilter
129
+    {
130
+        public bool LoadChildren { get; set; }
131
+        public bool IsPagingEnabled { get; set; }
132
+
133
+        public int Page { get; set; }
134
+        public int PageSize { get; set; }
135
+    }
136
+    
137
+    public class CustomerFilter : BaseFilter
138
+    {
139
+        public string Name { get; set; }
140
+        public string Email { get; set; }
141
+        public string Address { get; set; }
142
+    }
143
+```
144
+
145
+定义`Specification`相关
146
+
147
+```
148
+namespace Sample.Core.Specifications
149
+{
150
+    public class CustomerSpec : Specification<Customer>
151
+    {
152
+        public CustomerSpec(CustomerFilter filter)
153
+        {
154
+            Query.OrderBy(t => t.Name)
155
+                .ThenByDescending(t => t.Address);
156
+
157
+            if (filter.LoadChildren)
158
+                Query.Include(x => x.Stores);
159
+
160
+            if (filter.IsPagingEnabled)
161
+                Query.Skip(PaginationHelper.CalculateSkip(filter))
162
+                    .Take(PaginationHelper.CalculateTake(filter));
163
+
164
+            if (!string.IsNullOrEmpty(filter.Name))
165
+                Query.Where(t => t.Name == filter.Name);
166
+
167
+            if (!string.IsNullOrEmpty(filter.Email))
168
+                Query.Where(x => x.Email == filter.Email);
169
+
170
+            if (!string.IsNullOrEmpty(filter.Address))
171
+                Query.Search(x => x.Address, "%" + filter.Address + "%");
172
+        }
173
+    }
174
+}
175
+```
176
+
177
+实现`Ardalis.Specification`的一个接口
178
+
179
+```
180
+using Ardalis.Specification;
181
+
182
+namespace Sample.Core.Interfaces
183
+{
184
+    public interface IRepository<T> : IRepositoryBase<T> where T : class, IAggregateRoot
185
+    {
186
+    }
187
+}
188
+
189
+```
190
+
191
+# 基础设施层
192
+
193
+`IRepository`的实现
194
+
195
+```
196
+using Ardalis.Specification.EntityFrameworkCore;
197
+
198
+namespace Sample.Infra.Data
199
+{
200
+    public class MyRepository<T> : RepositoryBase<T>, IRepository<T> where T : class, IAggregateRoot
201
+    {
202
+        private readonly SampleDbContext dbContext;
203
+
204
+        public MyRepository(SampleDbContext dbContext) : base(dbContext)
205
+        {
206
+            this.dbContext = dbContext;
207
+        }
208
+
209
+        // Not required to implement anything. Add additional functionalities if required.
210
+    }
211
+}
212
+```
213
+
214
+注册
215
+
216
+```
217
+services.AddScoped(typeof(MyRepository<>));
218
+```
219
+
220
+# `Application`层或`UI`层调用
221
+
222
+```
223
+private readonly IRepository<Customer> customerRepository;
224
+var spec = new CustomerSpec();
225
+var customers = await customerRepository.ListAsync(spec);
226
+```
227
+
228
+
229
+
230
+源码位置:`F:\demos\CleanArchitecture\UseSpecification`

专题/后端/ddd1.png → 专题/后端/DDD/ddd1.png View File


专题/后端/ddd10.png → 专题/后端/DDD/ddd10.png View File


专题/后端/ddd11.png → 专题/后端/DDD/ddd11.png View File


专题/后端/ddd2.png → 专题/后端/DDD/ddd2.png View File


专题/后端/ddd3.png → 专题/后端/DDD/ddd3.png View File


专题/后端/ddd4.png → 专题/后端/DDD/ddd4.png View File


专题/后端/ddd5.png → 专题/后端/DDD/ddd5.png View File


专题/后端/ddd6.png → 专题/后端/DDD/ddd6.png View File


专题/后端/ddd7.png → 专题/后端/DDD/ddd7.png View File


专题/后端/ddd8.png → 专题/后端/DDD/ddd8.png View File


专题/后端/ddd9.png → 专题/后端/DDD/ddd9.png View File


+ 239
- 0
专题/后端/测试/04单元测试基本面.md View File

1
+# 为什么测试这么重要
2
+
3
+`人非圣贤孰能无过`,归根结底,代码是人写的,`Bug`终究是无法避免的。测试的目的是为了尽早发现问题,尽量减少`Bug`数量。
4
+
5
+# 测试类型
6
+
7
+## `Smoke test`
8
+
9
+- 程序员自己测试
10
+
11
+## `Unit Tests`
12
+
13
+- 通常由程序员做
14
+- 快,几毫秒
15
+- 独立
16
+- 一次测试一种行为
17
+- 不依赖于外界,比如使用`Moq`和`nSubstitute`
18
+
19
+## `Integration Tests`
20
+
21
+- 通常由程序员做
22
+- 相对较慢
23
+- 依赖于外部实现,比如依赖数据库、外部服务
24
+
25
+## `Functional Tests`
26
+
27
+- 从用户的角度测试功能,可能是内测人员
28
+- 可以手动测试,也有自动测试框架
29
+
30
+## `Subcutaneous Tests`
31
+
32
+- 最接近`UI`下的测试
33
+- 通常由程序员做
34
+- 适合逻辑在后端业务层,比如前后端分离的后端业务层
35
+
36
+## `Load Tests`
37
+
38
+- 通常由程序员做
39
+- 模拟程序的负载
40
+- 通常查看各项性能指标
41
+
42
+`Stress Tests`
43
+
44
+- 通常由程序员做
45
+- 通常查看`CPU`,`Network`,`Memory`指标
46
+
47
+![t3](F:\SourceCodes\DDWiki\专题\后端\测试\t3.png)
48
+
49
+# 单元测试的要和不要
50
+
51
+要
52
+
53
+- 要间接测试私有方法
54
+- 要符合条件/不符合条件的输入
55
+- 容易出问题的代码要单元测试,比如包含正则表达式
56
+- 很难被捕捉的异常要单元测试,比如路由、算法
57
+
58
+不要
59
+
60
+- 不要100%的覆盖率,没太必要
61
+- 不要给依赖组件的运行时错误进行单元测试
62
+- 不要在数据库架构、外部服务方面进行单元测试
63
+- 性能测试不通过单元测试
64
+- 代码生成器生成的代码不需要单元测试
65
+- 如果测试代码远大于被测试代码不需要单元测试
66
+
67
+# 一定要让测试不通过
68
+
69
+不好的
70
+
71
+```
72
+[Test]
73
+public void ShouldAddTwoNumbers()
74
+{
75
+   var calculator = new Calculator();
76
+   var result = calculator.Sum(10, 11);
77
+   Assert.Equal(21, result);
78
+}
79
+
80
+// The method to test in class Calculator ...
81
+public int Sum(int x, int y)
82
+{
83
+   throw new NotImplementedException();
84
+}
85
+```
86
+
87
+
88
+
89
+好的
90
+
91
+```
92
+[Test]
93
+public void ShouldAddTwoNumbers()
94
+{
95
+   var calculator = new Calculator();
96
+   var result = calculator.Sum(10, 11);
97
+   Assert.Equal(21, result);
98
+}
99
+
100
+// The method to test in class Calculator ...
101
+public int Sum(int x, int y)
102
+{
103
+   return 0;//返回一个值让不通过
104
+}
105
+```
106
+
107
+# 通过单元测试消除`Bug`
108
+
109
+如果通过单元测试发现一个`Bug`, 即单元测试显示红灯。重构代买,单元测试通过,显示绿色。于是,单元测试起到了帮助代码重构的作用。
110
+
111
+
112
+
113
+# 流行的测试框架
114
+
115
+- `NUnit`: 是`.NET`开源的、被认可的测试框架,有`UI`
116
+- `XUnit`: 来之`NUnit`作者,最新的,鼓励`TDD`的开发方式,甚至`.NET Core`团队也在使用
117
+- `MSTest`: 微软的测试框架,无法在`build server`跑`CI/CD`
118
+
119
+# 持续集成服务器
120
+
121
+监控源代码,一旦有变化,检查、构建、自动测试、发送报告等。
122
+
123
+# 测试项目的文件结构
124
+
125
+![t4](F:\SourceCodes\DDWiki\专题\后端\测试\t4.png)
126
+
127
+# 命名
128
+
129
+- `MethodName_StateUnderTest_ExpectedBehavior`
130
+
131
+```
132
+isAdult_AgeLessThan18_False
133
+withdrawMoney_InvalidAccount_ExceptionThrown
134
+admitStudent_MissingMandatoryFields_FailToAdmit
135
+```
136
+
137
+- `MethodName_ExpectedBehavior_StateUnderTest`
138
+- `test[Feature being tested]`
139
+
140
+```
141
+testIsNotAnAdultIfAgeLessThan18
142
+testFailToWithdrawMoneyIfAccountIsInvalid
143
+```
144
+
145
+- `Feature to be tested`
146
+
147
+```
148
+IsNotAnAdultIfAgeLessThan18
149
+FailToWithdrawMoneyIfAccountIsInvalid
150
+```
151
+
152
+- `Should_ExpectedBehaviour_When_StateUnderTest`
153
+
154
+```
155
+Should_ThrowException_When_AgeLessThan18
156
+Should_FailToWithdrawMoney_ForInvalidAccount
157
+```
158
+
159
+- `When_StateUnderTest_Expect_ExpectedBehavior`
160
+
161
+```
162
+When_AgeLessThan18_Expect_isAdultAsFalse
163
+When_InvalidAccount_Expect_WithdrawMoneyToFail
164
+```
165
+
166
+- `Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior`
167
+
168
+```
169
+Given_UserIsAuthenticated_When_InvalidAccountNumberIsUsedToWithdrawMoney_Then_TransactionsWillFail
170
+```
171
+
172
+# `AAA`
173
+
174
+```
175
+[TestMethod]
176
+public void TestRegisterPost_ValidUser_ReturnsRedirect()
177
+{
178
+   // Arrange
179
+   AccountController controller = GetAccountController();
180
+   RegisterModel model = new RegisterModel()
181
+   {
182
+      UserName = "someUser",
183
+      Email = "goodEmail",
184
+      Password = "goodPassword",
185
+      ConfirmPassword = "goodPassword"
186
+   };
187
+   // Act
188
+   ActionResult result = controller.Register(model);
189
+   // Assert
190
+   RedirectToRouteResult redirectResult = (RedirectToRouteResult)result;
191
+   Assert.AreEqual("Home", redirectResult.RouteValues["controller"]);
192
+   Assert.AreEqual("Index", redirectResult.RouteValues["action"]);
193
+}
194
+```
195
+
196
+# 数据量很大很难测试时用单元测试
197
+
198
+被测代码,数量越大越难测,很有必要使用单元测试。
199
+
200
+```
201
+public decimal CalculateTotal(List<myitem> items)
202
+{
203
+	decimal total = 0.0m;
204
+	foreach(MyItem i in items)
205
+	{
206
+	 	total += i.UnitPrice * (i - i.Discount);
207
+	}
208
+	return total;
209
+}
210
+```
211
+
212
+# 保持被测方法不要过于复杂
213
+
214
+# `Test Explorer`的使用
215
+
216
+![t5](F:\SourceCodes\DDWiki\专题\后端\测试\t5.png)
217
+
218
+![t6](F:\SourceCodes\DDWiki\专题\后端\测试\t6.png)
219
+
220
+
221
+
222
+# 对被测代码复杂逻辑的封装
223
+
224
+不好
225
+
226
+```
227
+while ((ActiveThreads > 0 || AssociationsQueued > 0) && (IsRegistered || report.TotalTargets <= 1000 )
228
+&& (maxNumPagesToScan == -1 || report.TotalTargets < maxNumPagesToScan) && (!CancelScan))
229
+```
230
+
231
+好的
232
+
233
+```
234
+while (!HasFinishedInitializing (ActiveThreads, AssociationsQueued, IsRegistered,
235
+report.TotalTargets, maxNumPagesToScan, CancelScan))
236
+```
237
+
238
+# 使用`Selenium`进行`Web`测试
239
+

专题/后端/2020.10.27单元测试前奏--解耦.md → 专题/后端/测试/2020.10.27单元测试前奏--解耦.md View File


专题/后端/2020.10.29单元测试Why.md → 专题/后端/测试/2020.10.29单元测试Why.md View File


专题/后端/2020.11.1单元测试使用xunit.md → 专题/后端/测试/2020.11.1单元测试使用xunit.md View File


BIN
专题/后端/测试/t3.png View File


BIN
专题/后端/测试/t4.png View File


BIN
专题/后端/测试/t5.png View File


BIN
专题/后端/测试/t6.png View File


Loading…
Cancel
Save