-
Notifications
You must be signed in to change notification settings - Fork 0
/
README.html
399 lines (368 loc) · 18.4 KB
/
README.html
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
<body>
<div style="font-size: medium;">
<!-- <h2 id="無回傳值方法-void-method-的單元測試-以-serilog-套件為例">無回傳值方法 (void method) 的單元測試, 以 Serilog 套件為例</h2> -->
<p>Unit Test for void method (Serilog package) in ASP.NET Core 6 MVC</p>
<h2 id="前言">前言</h2>
<p>接續前一篇 <a href="https://www.jasperstudy.com/2024/01/static-elements-system.html" target="_blank">樂透開獎(含日期限制)(含主辦人宣佈啟動開獎)</a> 的例子, 假設有一個新的需求: "需要將開獎的各次號碼及結果寫入Log記錄檔, 以供後續稽核".</p>
<p>雖然 ASP.NET Core 有提供一個 ILogger 的實作, 但功能有限. (參考文件4..)<br />
一般會採用 Serilog 套件, 但其相關的 Log method (例如: LogTrace, LogDebug ... 等), 都沒有回傳值 (void), 無法以回傳值模擬其結果, 那應該要如何建立測試呢?</p>
<p>關於 Serilog 的部份, 主要採 參考文件1..及2.. 方式進行演練及實作.</p>
<p><a href="https://github.com/jasper-lai/20240124_ASPNetCore6VoidLog" target="_blank">完整範例可由 GitHub 下載.</a></p>
<!--more-->
<h2 id="演練細節">演練細節</h2>
<h3 id="步驟_1-安裝以下套件">步驟_1: 安裝以下套件</h3>
<ul>
<li>Serilog.AspNetCore 8.0.0:
<ul>
<li>請留意它會更新一些 ASP.NET Core 內建套件的版本. 例如: Microsoft.Extensions.DependencyInjection 由 6.0.0 --> 8.0.0</li>
</ul>
</li>
</ul>
<pre><code class="language-csharp">更新:
Microsoft.Extensions.DependencyInjection.6.0.0 -> 8.0.0
Microsoft.Extensions.DependencyInjection.Abstractions.6.0.0 -> 8.0.0
System.Diagnostics.DiagnosticSource.4.3.0 -> 8.0.0
System.Text.Encodings.Web.6.0.0 -> 8.0.0
System.Text.Json.6.0.0 -> 8.0.0
正在安裝:
Microsoft.Extensions.Configuration.Abstractions.8.0.0
Microsoft.Extensions.Configuration.Binder.8.0.0
Microsoft.Extensions.DependencyModel.8.0.0
Microsoft.Extensions.Diagnostics.Abstractions.8.0.0
Microsoft.Extensions.FileProviders.Abstractions.8.0.0
Microsoft.Extensions.Hosting.Abstractions.8.0.0
Microsoft.Extensions.Logging.8.0.0
Microsoft.Extensions.Logging.Abstractions.8.0.0
Microsoft.Extensions.Options.8.0.0
Microsoft.Extensions.Primitives.8.0.0
Serilog.3.1.1
Serilog.AspNetCore.8.0.0
Serilog.Extensions.Hosting.8.0.0
Serilog.Extensions.Logging.8.0.0
Serilog.Formatting.Compact.2.0.0
Serilog.Settings.Configuration.8.0.0
Serilog.Sinks.Console.5.0.0
Serilog.Sinks.Debug.2.0.0
Serilog.Sinks.File.5.0.0
</code></pre>
<h3 id="步驟_2-初始化-serilog">步驟_2: 初始化 Serilog</h3>
<p>以下設定, 會同時輸出到 Console 及 File.</p>
<pre><code class="language-csharp">#region 初始化 Serilog 設定
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.WriteTo.Console()
.WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
//The builder.Host.UseSerilog() call will redirect all log events through your Serilog pipeline.
builder.Host.UseSerilog();
#endregion
</code></pre>
<h3 id="步驟_3-在-lottoservice-使用-serilog">步驟_3: 在 LottoService 使用 Serilog</h3>
<p>1.. 修改建構子, 加入 ILogger<LottoService> 物件的注入.</p>
<pre><code class="language-csharp">private readonly IRandomGenerator _randomGenerator;
private readonly IDateTimeProvider _dateTimeProvider;
private readonly IFileSystem _fileSystem;
private readonly ILogger<LottoService> _logger;
public LottoService(IRandomGenerator randomGenerator, IDateTimeProvider dateTimeProvider
, IFileSystem fileSystem, ILogger<LottoService> logger)
{
_randomGenerator = randomGenerator;
_dateTimeProvider = dateTimeProvider;
_fileSystem = fileSystem;
_logger = logger;
}
</code></pre>
<p>2.. 修改 Lottoing() 方法, 加入寫至 Log 的程式段.</p>
<pre><code class="language-csharp">public LottoViewModel Lottoing(int min, int max)
{
var result = new LottoViewModel();
var jsonOptions = new JsonSerializerOptions()
{
////中文字不編碼
//Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
//允許基本拉丁英文及中日韓文字維持原字元
Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs),
//不進行換行與縮排
WriteIndented = false,
//字首處理小寫
//PropertyNamingPolicy = null //不轉小寫
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // 轉小寫
};
string myJson;
// -----------------------
// 檢核1: 是否為每個月 5 日
// -----------------------
var now = _dateTimeProvider.GetCurrentTime();
if (now.Day != 5)
{
result.Sponsor = string.Empty;
result.YourNumber = -1;
result.Message = "非每個月5日, 不開獎";
//序列化 (by System.Text.Json) 後寫到 Log
myJson = JsonSerializer.Serialize(result, jsonOptions);
//#pragma warning disable CA2254 // Template should be a static expression
// _logger.LogCritical(myJson);
//#pragma warning restore CA2254 // Template should be a static expression
_logger.LogCritical("{myJson}", myJson);
//
return result;
}
// -----------------------
// 檢核2: 主辦人員是否已按下[開始]按鈕
// -----------------------
// 註: 這裡有可能會出現一些 Exception, 例如: FileNotFoundException
var sponsor = string.Empty;
try
{
sponsor = _fileSystem.File.ReadAllText("Extras/startup.txt");
}
catch (Exception)
{
result.Sponsor = sponsor;
result.YourNumber = -2;
result.Message = "主辦人員尚未按下[開始]按鈕";
//序列化 (by System.Text.Json) 後寫到 Log
myJson = JsonSerializer.Serialize(result, jsonOptions);
_logger.LogError("{myJson}", myJson);
//
return result;
}
// Random(min, max): 含下界, 不含上界
var yourNumber = _randomGenerator.Next(min, max);
// 只要餘數是 9, 就代表中獎
var message = (yourNumber % 10 == 9) ? "恭喜中獎" : "再接再厲";
result.Sponsor = sponsor;
result.YourNumber = yourNumber;
result.Message = message;
//序列化 (by System.Text.Json) 後寫到 Log
myJson = JsonSerializer.Serialize(result, jsonOptions);
_logger.LogInformation("{myJson}", myJson);
//
return result;
}
</code></pre>
<h3 id="步驟_4-修改原有的測試案例">步驟_4: 修改原有的測試案例</h3>
<p>1.. 因為 LottoService 的建構子增加了 ILogger<LottoService> 這個參數, 所以, 原有的測試案例, 也要跟著改, 不然會編譯失敗.</p>
<p>2.. 因為 LogTrace, LogDebug, ... 沒有回傳值, 所以, 只能用被呼叫的次數作為驗證的指標.</p>
<p>3.. 注意: LogInformation() 是擴充方法, 不能直接 Verify !</p>
<pre><code class="language-csharp">[TestMethod()]
public void Test_Lottoing_今天是20240105_主辦人宣告開始_輸入亂數範圍_0_10_預期回傳_9_恭喜中獎()
{
// Arrange
var expected = new LottoViewModel()
{ Sponsor = "傑士伯", YourNumber = 9, Message = "恭喜中獎" }
.ToExpectedObject();
int fixedValue = 9;
DateTime today = new(2024, 01, 05);
var mockRandomGenerator = new Mock<IRandomGenerator>();
var mockDateTimeProvider = new Mock<IDateTimeProvider>();
mockRandomGenerator.Setup(r => r.Next(It.IsAny<int>(), It.IsAny<int>())).Returns(fixedValue);
mockDateTimeProvider.Setup(d => d.GetCurrentTime()).Returns(today);
// [檔案系統]
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"Extras/startup.txt", new MockFileData("傑士伯") },
}
);
// [Logger]
var mockLogger = new Mock<ILogger<LottoService>>();
// Act
var target = new LottoService(mockRandomGenerator.Object, mockDateTimeProvider.Object, mockFileSystem, mockLogger.Object);
var actual = target.Lottoing(0, 10);
// Assert
//---------------
//LogInformation() 是擴充方法, 不能直接 Verify !
//---------------
//mockLogger.Verify(x => x.LogInformation(It.IsAny<string>()), Times.Once());
mockLogger.Verify(
x => x.Log(
LogLevel.Information, // Match the log level
It.IsAny<EventId>(), // Use It.IsAny for EventId
It.Is<It.IsAnyType>((v, t) => true), // Match any log message
It.IsAny<Exception?>(), // Use It.IsAny for Exception (nullable)
It.IsAny<Func<It.IsAnyType, Exception?, string>>() // Use It.IsAny for the message formatter Func (nullable)
),
Times.Once);
expected.ShouldEqual(actual);
}
</code></pre>
<pre><code class="language-csharp">[TestMethod()]
public void Test_Lottoing_今天是20240105_主辦人宣告開始_輸入亂數範圍_0_10_預期回傳_1_再接再厲()
{
// Arrange
var expected = new LottoViewModel()
{ Sponsor="傑士伯", YourNumber = 1, Message = "再接再厲" }
.ToExpectedObject();
int fixedValue = 1;
DateTime today = new(2024, 01, 05);
var mockRandomGenerator = new Mock<IRandomGenerator>();
var mockDateTimeProvider = new Mock<IDateTimeProvider>();
mockRandomGenerator.Setup(r => r.Next(It.IsAny<int>(), It.IsAny<int>())).Returns(fixedValue);
mockDateTimeProvider.Setup(d => d.GetCurrentTime()).Returns(today);
// [檔案系統]
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"Extras/startup.txt", new MockFileData("傑士伯") },
}
);
// [Logger]
var mockLogger = new Mock<ILogger<LottoService>>();
// Act
var target = new LottoService(mockRandomGenerator.Object, mockDateTimeProvider.Object, mockFileSystem, mockLogger.Object);
var actual = target.Lottoing(0, 10);
// Assert
mockLogger.Verify(
x => x.Log(
LogLevel.Information, // Match the log level
It.IsAny<EventId>(), // Use It.IsAny for EventId
It.Is<It.IsAnyType>((v, t) => true), // Match any log message
It.IsAny<Exception?>(), // Use It.IsAny for Exception (nullable)
It.IsAny<Func<It.IsAnyType, Exception?, string>>() // Use It.IsAny for the message formatter Func (nullable)
),
Times.Once);
expected.ShouldEqual(actual);
}
</code></pre>
<pre><code class="language-csharp">[TestMethod()]
public void Test_Lottoing_今天是20240122_不論主辦人是否宣告開始_輸入亂數範圍_0_10_預期回傳_負1_非每個月5日_不開獎()
{
// Arrange
var expected = new LottoViewModel()
{ Sponsor = "", YourNumber = -1, Message = "非每個月5日, 不開獎" }
.ToExpectedObject();
int fixedValue = 9;
DateTime today = new(2024, 01, 22);
var mockRandomGenerator = new Mock<IRandomGenerator>();
var mockDateTimeProvider = new Mock<IDateTimeProvider>();
mockRandomGenerator.Setup(r => r.Next(It.IsAny<int>(), It.IsAny<int>())).Returns(fixedValue);
mockDateTimeProvider.Setup(d => d.GetCurrentTime()).Returns(today);
// [檔案系統]
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ @"Extras/startup.txt", new MockFileData("傑士伯") },
}
);
// [Logger]
var mockLogger = new Mock<ILogger<LottoService>>();
// Act
var target = new LottoService(mockRandomGenerator.Object, mockDateTimeProvider.Object, mockFileSystem, mockLogger.Object);
var actual = target.Lottoing(0, 10);
// Assert
mockLogger.Verify(
x => x.Log(
LogLevel.Critical, // Match the log level
It.IsAny<EventId>(), // Use It.IsAny for EventId
It.Is<It.IsAnyType>((v, t) => true), // Match any log message
It.IsAny<Exception?>(), // Use It.IsAny for Exception (nullable)
It.IsAny<Func<It.IsAnyType, Exception?, string>>() // Use It.IsAny for the message formatter Func (nullable)
),
Times.Once);
expected.ShouldEqual(actual);
}
</code></pre>
<pre><code class="language-csharp">[TestMethod()]
public void Test_Lottoing_今天是20240105_但主辦人常未宣告開始_輸入亂數範圍_0_10_預期回傳_負2_主辦人員尚未按下開始按鈕()
{
// Arrange
var expected = new LottoViewModel()
{ Sponsor = "", YourNumber = -2, Message = "主辦人員尚未按下[開始]按鈕" }
.ToExpectedObject();
int fixedValue = 1;
DateTime today = new(2024, 01, 05);
var mockRandomGenerator = new Mock<IRandomGenerator>();
var mockDateTimeProvider = new Mock<IDateTimeProvider>();
mockRandomGenerator.Setup(r => r.Next(It.IsAny<int>(), It.IsAny<int>())).Returns(fixedValue);
mockDateTimeProvider.Setup(d => d.GetCurrentTime()).Returns(today);
// [檔案系統]
var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
{
//只要不提供檔案路徑, 就會視為 FileNotFound Exception
//{ @"startup.txt", new MockFileData("傑士伯") },
}
);
// [Logger]
var mockLogger = new Mock<ILogger<LottoService>>();
// Act
var target = new LottoService(mockRandomGenerator.Object, mockDateTimeProvider.Object, mockFileSystem, mockLogger.Object);
var actual = target.Lottoing(0, 10);
// Assert
mockLogger.Verify(
x => x.Log(
LogLevel.Error, // Match the log level
It.IsAny<EventId>(), // Use It.IsAny for EventId
It.Is<It.IsAnyType>((v, t) => true), // Match any log message
It.IsAny<Exception?>(), // Use It.IsAny for Exception (nullable)
It.IsAny<Func<It.IsAnyType, Exception?, string>>() // Use It.IsAny for the message formatter Func (nullable)
),
Times.Once);
expected.ShouldEqual(actual);
}
</code></pre>
<h3 id="步驟_5-檢查式執行的結果">步驟_5: 檢查式執行的結果</h3>
<p><img src="https://github.com/jasper-lai/20240124_ASPNetCore6VoidLog/blob/master/pictures/10-ConsoleLog.png?raw=true" alt="ConsoleLog" /></p>
<h3 id="步驟_6-檢查測試的結果">步驟_6: 檢查測試的結果</h3>
<p><img src="https://github.com/jasper-lai/20240124_ASPNetCore6VoidLog/blob/master/pictures/11-TestResult.png?raw=true" alt="TestResult" /></p>
<h2 id="結論">結論</h2>
<p>Serilog 為 ASP.NET Core 6 的常用套件, 故本篇以此為標的, 進行演練.</p>
<p>過程中遇到了一些狀況, 但也算有收穫.<br />
1.. LogTrace, LogDebug ... 為 extension method, 不能直接 Verify, 必須改用其實體的 Log() 方法作為 Verify 的對象.<br />
2.. 輸出至 Log 的中文字會被編碼, 主要原因為於 JSON 序列化時, 預設會編碼, 必須透過 json options, 指定不進行編碼.</p>
<h2 id="參考文件">參考文件</h2>
<ul>
<li><a href="https://blog.miniasp.com/post/2021/11/29/How-to-use-Serilog-with-NET-6" target="_blank">1.. (Will保哥) .NET 6.0 如何使用 Serilog 對應用程式事件進行結構化紀錄</a></li>
<li><a href="https://gist.github.com/doggy8088/32f7c179f06ab0616b2e32728f734c5b" target="_blank">2.. (Will保哥) Serilog 與 ASP.NET Core 6.0 範例</a></li>
<li><a href="https://www.c-sharpcorner.com/article/how-to-implement-serilog-in-asp-net-core-web-api/" target="_blank">3.. (C# Cornor) How To Implement Serilog In ASP.NET Core Web API</a></li>
<li><a href="https://github.com/serilog/serilog-aspnetcore" target="_blank">4.. (GitHub) Serilog.AspNetCore 原始程式碼</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-6.0" target="_blank">5.. (Microsoft Learn) Logging in .NET Core and ASP.NET Core</a></li>
</ul>
<blockquote>
<p>The default ASP.NET Core web app templates:
Use the Generic Host.
Call WebApplication.CreateBuilder, which adds the following logging providers:</p>
<ul>
<li>Console</li>
<li>Debug</li>
<li>EventSource</li>
<li>EventLog: Windows only</li>
</ul>
</blockquote>
<ul>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-6.0" target="_blank">6.. (Microsoft Learn) .NET Generic Host in ASP.NET Core</a></li>
</ul>
<blockquote>
<p>A host is an object that encapsulates an app's resources, such as:</p>
<ul>
<li>Dependency injection (DI)</li>
<li>Logging</li>
<li>Configuration</li>
<li>IHostedService implementations</li>
</ul>
</blockquote>
<ul>
<li><a href="https://stackoverflow.com/questions/70955861/the-logging-message-template-should-not-vary-between-calls-ca2254-when-only-pa" target="_blank">6.. (StackOverflow) The logging message template should not vary between calls (CA2254) when only passing on variables</a></li>
<li><a href="https://stackoverflow.com/questions/66307477/how-to-verify-iloggert-log-extension-method-has-been-called-using-moq" target="_blank">7.. (StackOverflow) How to verify ILogger<T>.Log extension method has been called using Moq?</a></li>
</ul>
<pre><code class="language-csharp">// You need to verify like this
_loggerMock.Verify(logger => logger.Log(
It.Is<LogLevel>(logLevel => logLevel == LogLevel.Error),
It.Is<EventId>(eventId => eventId.Id == 0),
It.Is<It.IsAnyType>((@object, @type) => @object.ToString() == "myMessage" && @type.Name == "FormattedLogValues"),
It.IsAny<Exception>(),
It.IsAny<Func<It.IsAnyType, Exception, string>>()),
Times.Once);
</code></pre>
<ul>
<li>
<p><a href="https://rainmakerho.github.io/2022/07/09/aspnet6-custom-logger/" target="_blank">8.. (亂馬客) ASP.NET Core 6 實作 Logger</a></p>
</li>
<li>
<p><a href="https://blog.darkthread.net/blog/aspnet-core-json-setting/" target="_blank">9.. (黑暗執行緒) ASP.NET Core JSON 中文編碼問題與序列化參數設定</a></p>
</li>
</ul>
<blockquote>
<p>這篇所述的作法, 只適用在 Controller; 若為 Service, 要自己寫.</p>
</blockquote>
</div>
</body>