[閱讀筆記] 回饋隨筆—《SCRUM敏捷實戰手冊》

前情提要

朋友在一個知識型部落客的團隊中工作,
作了一個有關 Scrum 的影片,
恰巧我最近的工作與 Scrum 的導入有比較深的關係,
影片我也蠻喜歡的,就稍微作個筆記當作回饋。

回饋隨筆

第一點,讓我可以看到非業界人員初探 Scrum 的角度,
我第一次「知道」Scrum 已經是在 C 社的事了。
有點忘了第一次接觸的「初心」,台灣非軟體人說這方面的非常稀少。
能夠提供不同視角是很重要的。

橄欖球列陣這部份我蠻喜歡的; 雖然直譯就知道 Scrum 原本的意思,
但是離「光速蒙面俠」太遠,已經有點忘了那些規則,
現在對比起來真的很像,為了一次的達陣,你需要多少衝刺 ?
戰術又該怎麼選擇 ? 球員平時的訓練與臨場反應是不是也可以映射到這個業界呢 ?

整個影片很淺顯易懂,口齒清晰即使放到 1.5 倍速還是聽得清楚,
可惜的是汽車廠 5 年轉型的故事我想知道更多細節或出處,
不是很喜歡「一半的時間,作兩倍的工作」的結論,
我覺得是書商的行銷手段,不是 Scrum 的目標與強項。
產能的提昇不過是個結果,重點是三大支柱對團隊帶來的效應。
沒提到透明性、檢視性與調適性我覺得蠻可惜的,

另外個人的工作流程我不覺得需要跑 Scrum ,
可以試試 GTD 或 PDCA ,而且你會發現很多類似的觀念與原則。
Scrum 設計上是適合小型團隊的,業界說法是 3~9 人,不含 PO 與 SM
多人的公司也有很多別的方法論(EX: LeSS or SAFe),這裡我不多開戰線,敏捷無它「務實」而已。
方法沒有不好,只有適不適合。

差異

有些用字跟業界不太一樣有點可惜,硬要翻譯不如保留英文或是業界常用翻譯。
Scrum 跟規格固定應該沒有什麼關係,不過工程上的確這樣作會帶來一些好處。
Scrum 跟 Agile 本質上還是有些不同, Agile 比較像願景、原則比較抽象的層次。
複習一下 Agile 的宣言

  • Individuals and interactions over processes and tools
  • Working software over comprehensive documentation
  • Customer collaboration over contract negotiation
  • Responding to change over following a plan

Scrum 是業界較常見的框架而且與其它工作法相容性非常的高,
就我個人而言擷取過 XP、GTD、番茄鐘、ORID 與 ToC 等方法。
作為一個框架,有彈性的包容各種方法是它如此廣佈的原因之一。

最後 Scrum 無法擺脫加班、Scrum 無法擺脫加班、Scrum 無法擺脫加班
不要盲從,Google 是先成為 google 才有導入 Scrum,你導入 Scrum 也不會成為 Google。
丟書前多想 3 秒鐘,丟桌上不如丟臉上,丟書不如丟辭呈。

參考

(fin)

[A社筆記] Introduce Unit Test --- 心法篇

Why Unit Test 心法篇

網路上很多,自已找(兇)。

我的想法 about Unit Test

  • 有驗收才有品質,所以我需要測試
    • 黑箱
    • 白箱
    • 整合
    • 單元
  • UT 不過是驗收的最基本的單位。
  • 開發人員一定會測試的,不論用何種方法
    • Debuger
    • Console Output
    • Break Point
  • 程式人員的好品德
    • Laziness
    • Impatience
    • Hubris
  • 程式是照你寫的跑,而不是照你想的跑

  • 既然你會測試,那麼為什麼不讓它可以[重複/一鍵]被執行

  • 只是為了重複執行,何不使用現有的測試框架與工具?

先訂驗收標準,再進行開發是正常不過的作法,
只不過你太聰明而在腦中測試過了。
但是程式是照你寫的跑,而不是照你想的跑
不如先寫下驗收標準(測試左移/DoD/TDD),再進行開發。
錦上添花的話,就透過工具讓「執行測試」可以快速重複。
剩下的問題是,這些驗收標準寫到多「鉅細靡遺」?
有沒有什麼方式可以提昇我撰寫的速度 ?

假設我有了測試保護,那麼重構將是一件安全的事。
壞味道可以給我提示,而 Design Pattern 可以是改善程式的一個指引。

Unit Test

3A

程式寫的是 AAA (正序)心裡想的是AAA(逆序)

  • Arrange
  • Act
  • Asset

紅、綠、重構

如果以 TDD 開發,寫完新的測試後,得到的一個「綠燈」反而是一個壞味道。

第一天分享

pair programming 30 min,QA 約 20分鐘。
先請同事實作 1+1 的 Unit Test 看一下他對測試的理解。

  • 建立測試專案,可以選用 xUnit

    1
    2
    不選 MsTest 的理由是,我比較喜歡建構子與解構子的寫法,  
    勝過 TestInitialize/TestCleanup 的 Attribute 的寫法
  • 引導由測試寫出方法。

    1
    2
    3
    logic → function → class method  
    透過寫測試讓 Production 在思考中產生
    引導的沒有很成功
  • 3A 的寫法,未來會介紹沒有 3A 的寫法(為了更好的理解)

  • 方法測試案例的命名

  • 介紹 Assert.Equal 取代 Debug.Assert

  • 簡單提到三種邏輯,回傳值、改變值、互動。

  • 為什麼我討厭 Static

[未排序]預計要講的題目

  • 範例是否為偶數 → 牛奶是否過期 → 如何透過一些手法,控制不可控的類別
  • 怎麼對 Legacy Code 作解耦 ?
  • 介紹 Mock Framework
  • 怎麼寫出好理解的 Assertion ?
  • 介紹 Assert Framework
  • 建立 A 社的道場 Repo
  • more …

問題

  • 開發後,自已會完全不自已測試就丟給 QA 或客戶嗎 ?
  • 什麼是 Dojo ?
    • 日文的道場,把寫程式想像成是在練功,建立一個練功的環境。
  • 什麼是 Kata ?
    • 日文的形,或是可以說是套路/招式,一樣透過練習招式來強化自已的能力。

參考

(fin)

[實作筆記] Elasticsearch Insert Data with .Net

前篇

上次使用 Nlog 直接與 ElasticSearch 作結合,
這次來看看怎麼賽入資料給 ElasticSearch 。

NEST

一般來說,ElasticSearch 只要透過呼叫 API 就可
但是我將使用 C# 的 Nuget 套件 NEST 來簡化呼叫 API 的行為。

步驟

安裝套件

1
Install-Package NEST

建立連線

1
2
3
var node = new Uri("http://localhost:9200");
var settings = new ConnectionSettings(node);
var client = new ElasticClient(settings);

寫入資料

1
2
3
4
5
6
7
var json = new
{
name = "test name",
timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK")
};
string indexName = $"test-index-{DateTime.Now:yyyy.MM.dd}";
client.Index(json, idx => idx.Index(indexName));

Create Index Pattern

連線進入 Kibana (http://localhost:5601/),

Setting > Kibana > Index patterns > Create index pattern

Step 1 of 2: Define index pattern
輸入 test-index-*

Step 2 of 2: Configure settings
記得選取時間軸(x軸)為 timestamp,這裡會透過 automap 對應欄位,
故在產生測試資料時,記得先產出時間資料,
這個範例我使用的格式為

1
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK")

設計共用型別

1
2
3
4
5
6
public class Record
{
public DateTime Created { get; }
public string Type { get; }
public Object Record { get; }
}

想法是通用類別,或許用抽像類別繼承也可以 ?
Created 欄位用來記錄資料產生時間,
Type 用來記錄 Record 的型別,
Record 用來記錄實際的資料。

預計未來可能會需要取出 Json 資料再轉成物件操作。

大概就醬。

6/8 補充 .Net Logger 分級

Trace 0 包含最詳細訊息的記錄。 這些訊息可能包含敏感性應用程式資料。 這些訊息預設會停用,且永遠不應在生產環境中啟用。
Debug 1 開發期間用於互動式調查的記錄。 這些記錄主要應包含適用於偵錯的資訊,且不具備任何長期價值。
Information 2 追蹤應用程式一般流程的記錄。 這些記錄應具備長期值。
Warning 3 醒目提示應用程式流程中異常或未預期事件的記錄,這些異常或未預期事件不會造成應用程式執行停止。
Error 4 在目前執行流程因失敗而停止時進行醒目提示的記錄。 這些記錄應指出目前活動中的失敗,而非整個應用程式的失敗。
Critical 5 描述無法復原的應用程式或系統損毀,或需要立即注意重大失敗的記錄。
None 6 不會用來寫入記錄訊息。 指定記錄類別不應寫入任何訊息。

(fin)

[A社筆記] 閒聊 Product Backlog

前言

Although implementing only parts of Scrum is possible, the result is not Scrum.

雖然實施部分的 Scrum 是可能的,但結果並不是 Scrum 《Scrum Guide》

之所以在文章一開頭就引述這段文字,是因為世界上太多 Scrum But 了;
而我下文所敘的則是 Not Scrum
未來有機會再深入討論兩者的差異,對我來說主要是心態上的差異。

我們透明、檢核、調試,我們擁抱改變,我們有所堅持,我們有理想但我們也務實。
好啦,說不定只有你自已。

在 A 社的導入的過程中,我開場白通常會說「這不是 Scurm」然後開始拿 Scurm 的東西說嘴。
至少對我來說,這比那些號稱「我們跑 Scrum」「But #^%!~…」要好得多,
與其讓你難以分辨斷句在哪,或是誤會怎麼 Scrum 這麼糞,不如讓我明確的告訴你「這不是 Scurm」。

背景

蠻小巧的團隊,小到我覺得也許不用任何「敏捷」
分為兩個組成 QA 部門與 RD 部門,遠端工作的 RD 主管兼職 PM
QA 團隊的主管除了目前這個團隊,也要兼者作其它團隊的測試,
此外還有一個大主管,在國外有時差,每周固定會與成員們開 3 次的會。
RD 團隊成為會有一個 Daily Sync 的會議,每天約 5~10 分鐘。
整個團隊有使用 Azure DevOps 的看板,但是 QA 只會用來開 Bug。
但不知道什麼時候 Bug 才會被修復。

問題

Azure DevOps BackLogs 太像文件

Azure DevOps 的 BackLogs 放著許多 Feature ,
依據不同的功能,再將每個 Sprint 的 User Story 放進去
這些 Feature 永遠不會被作完,隨著時間過去,
儘管完成了許多 User Story ,但是也會有新的 User Story 被加進去。
而作為文件,他的巢狀結構又不足以面對複雜的需求內容,
分散式存儲對於維護與修改上也是相同不便。

Bug 隨時蹦出

QA 的工作就是不斷的測試,一有發現問題就開立 Bug,
RD 有空就會去領來作,有時候也會有 RD 主管確認過後再分配給 RD
這導致不同面向的問題,RD 自領的情況可能會無序的作,
導致真正有價值的 Bug 無法第一時間被修正。
而如果每件事情都要 RD 主管確認後再進行動作,
RD 主管恐怕會變成瓶頸所在。

BackLogs 簡介

下面是我在向團隊介紹 Backlogs 後的筆記。

在 Azure DevOps 上會有 BackLogs 與 Sprints > Backlog。
恰巧與 Scurm 的 Product BackLogs 與 Sprints Backlog 可以一一對應。
如果以 XP 來說,可以投射到發佈計劃會議與迭代計劃會議中討論的「用戶故事清單」。
如果以 GTD 的方法論來說 BackLogs 可以當作收集一切事務的 Inbox,
而 Sprints > Backlog 可以視作專案裡面的工作項目。

總的來說,這些方法論的名詞故有所不同,但本質上卻是非常接近的事務。
我們總是有許多新的想法,但是沒有足夠的時間作所有的事。
所以我們必須補捉這些想法,放入 BackLogs 之中
補捉了之後,必須排序,這裡的概念有「重要且緊急」先作 ( 參考 Eisenhower Matrix ),
或是「價值高」的先作,或是有影響力的先作 ( 參考 Impact Mapping )。
GTD 的概念是 5 分鐘能完成的想法,立刻去作不然就丟到 BackLogs ( Inbox ) 之中,
我認為團隊不適合這樣作,畢竟 GTD 是針對個人的工作法。
團隊反而要刻意不作為,才能讓真正重要的事情浮現,而不是被一堆雜訊 DDos,
所有想法我的建議是「丟到 Backlogs 之中」,然後定期梳理吧。

理論上大部份事情會由模糊到清晰,這是個過程,之後才會進入到一個可以被執行的階段。
在 Scurm 之中,常用「遠光燈」作例子,我個人比較喜歡用「通勤」舉例。

留個問題給你,誰來衡量 ? 團隊 ? PO ? 客戶 ?

想像一下你要上班了,在家裡你對公司的方向與位置其實有個大概的想法,
要走過哪些路口,經過某個大樓,穿越了一座橋後你需要待轉,
再過幾個紅綠燈,看到速食店後就可以開始找車位了。
實際上路後,也許路上施工,你必需繞個路,或是運氣很好一路綠燈。
哦,不!! 你的車拋錨了,你必須改搭公車,而它的路線是……

有個大方向就是你的目標,路口、橋、大樓,就是你的每個迭代,
每次的煞車、轉彎或催油門就是再被細分下來的工作項目,
紅燈、拋錨、下雨天就是你真實上路後才會體驗到的事。
有個概念「Road Map」其實也是在說差不多的事。

回到 Product BackLogs 與 Sprints Backlog ,
我們也就是不斷的細化這些項目,一直到可執行然後被執行為止。

至於怎麼變快呢 ?
也許你要選一條最優路線,最短的距離最少的車流,沒有紅綠燈之類的,
也許你要有最棒的工具,最好的機車、定期保養之類的,
也許你要改變的開車習慣,不要危險駕駛、不任意切換車道等等…

透過的調整工作順序、找尋最佳工具輔助與保持正確心態,是我目前的答案。

(fin)

[實作筆記] Dotnet Logger 整合 Kibana Kibana 與 Elasticsearch

前情提要

我將 ASP.Net Core 的 API 服務加上 Log。
Logger 使用 NLog ,載體我使用 Elasticsearch ,
使用者操作介面使用 Kibana。

然後面對開發者,我希望用 AOP 方式,
讓 Logger 與主邏輯分離。

  • 這裡會用到簡單基本的 docker 技術
  • 透過 docker-compose 建立 Kibana 與 Elasticsearch
  • 假設你已經會使用 Dotnet Core DI 注入 Logger
  • 用最原生的方法實作 AOP
  • 對你可能沒有幫助

Run ElasticSearch & Kibana with docker

在本機建立 ElasticSearch & Kibana
首先建立 docker-compose.yaml 檔如下,
簡單說明一下內容:

  • 起一個 elasticsearch,port : 9200
  • 起一個 kibana ,port : 5601,設定環境變數 ELASTICSEARCH_URLhttp://localhost:9200
  • 網路名命為 elastic 使用 birdge 讓兩個 container 連起來
  • elasticsearch-data: 實務上我想需要指定一個 storage(硬碟或 File System 之類的)
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
version: '3.1'

services:

elasticsearch:
container_name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
ports:
- 9200:9200
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
environment:
- xpack.monitoring.enabled=true
- xpack.watcher.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.type=single-node
networks:
- elastic

kibana:
container_name: kibana
image: docker.elastic.co/kibana/kibana:7.6.2
ports:
- 5601:5601
depends_on:
- elasticsearch
environment:
- ELASTICSEARCH_URL=http://localhost:9200
networks:
- elastic

networks:
elastic:
driver: bridge

volumes:
elasticsearch-data:

執行

1
docker-compose up -d

完成後在本機瀏覽器瀏覽以下網址。
確定功能正常。

雷包

第一次啟動 Kibana 要 5 分鐘左右,但是我不確定是 docker 或是 Kibana 的問題

Dotnet Core NLog with ElasticSearch

首先必需安裝相關套件

1
2
3
4
5
Install-Package NLog.Web.AspNetCore

Install-Package NLog

Install-package NLog.Targets.ElasticSearch

設定 appsettings.json
(請見 20200518 的補充)

1
2
3
4
5
6
{
"ConnectionsString": {
"ElasticSearchServerAddress": "http://localhost:9200/"
},
///...
}

設定 nlog.config,注意以下路徑

  • nlog > extenstions > assembly
  • nlog > targets > target
  • nlog > rules > logger

這裡的重點是 ElasticSearchServerAddress 這組字串需設定成你的 ElasticSearchServerAddress
(請見 20200518 的補充)

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
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="c:\temp\internal-nlog.txt">

<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
<add assembly="NLog.Targets.ElasticSearch"/>
</extensions>

<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File" name="allfile" fileName="${currentdir}\log\nlog-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<!-- ElasticSearch -->
<target name="ElasticSearch"
xsi:type="ElasticSearch"
ConnectionStringName="ElasticSearchServerAddress"
index="dotnetcore-nlog-elk-${date:format=yyyy.MM.dd}"
documentType="logevent"
includeAllProperties="true"
layout="[${date:format=yyyy-MM-dd HH\:mm\:ss}][${level}] ${logger} ${message} ${exception:format=toString}">
<field name="MachineName" layout="${machinename}" />
<field name="Time" layout="${longdate}" />
<field name="level" layout="${level:uppercase=true}" />
<field name="logger" layout=" ${logger}" />
<field name="message" layout=" ${message}" />
<field name="exception" layout=" ${exception:format=toString}" />
<field name="processid" layout=" ${processid}" />
<field name="threadname" layout=" ${threadname}" />
<field name="stacktrace" layout=" ${stacktrace}" />
<field name="Properties" layout="${machinename} ${longdate} ${level:uppercase=true} ${logger} ${message} ${exception}|${processid}|${stacktrace}|${threadname}" />
</target>
</targets>

<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<logger name="*" minlevel="Trace" writeTo="ElasticSearch" />
</rules>
</nlog>

我的 Logger 代碼可能會類似這樣:
我想調整 Logger 代碼,不要與商務邏輯混在一起。

  • 一般的 Logger 我會用 AOP 的方式作成 Audit Log 記錄
  • catch Exception 的 Logger 我會統一處理
1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

public Result MyMethod(Context ctx)
{
this.logger.LogInformation("Hello Marsen");
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

20200518 補充

nlog.configappsettings.json 內容調整。

使用Elasticsearch Cloud(以下簡稱 ESC)當作服務的儲存體。
踩到了一個雷包,當我把 ElasticSearchServerAddress 修改成 ESC 服務的 EndPoint
我發現 ESC 並未接收到 Log ,更奇怪的事情是,在我本機端 docker 所建置服務仍然收到了 Log。

查詢了一下最新的Nlog ElasticSearch Wiki後,
應該改用 uri 屬性設定 EndPoint ,而沒有設定的情況下預設為 localhost:9200

1
2
uri - Uri of a Elasticsearch node. Multiple can be passed comma separated.  
Ignored if cloud id is provided. Layout Default: http://localhost:9200

而要使用 ESC 的 EndPoint,除了設定 uri 外,還需要啟用授權。
requireAuth 設定為 true(預設為false),另外還要設定 usernamepassword
appsettings.json 的 ConnectionsString 就可以刪除了。
nlog.config 修改大致如下

1
2
3
4
5
6
7
8
9
10
11
12
<!--略-->
<target xsi:type="ElasticSearch"
name="elastic.co"
index="dotnetcore-nlog-elk-${date:format=yyyy.MM.dd}"
documentType="_doc"
includeAllProperties="true"
layout="[${date:format=yyyy-MM-dd HH\:mm\:ss}][${level}] ${logger} ${message} ${exception:format=toString}"
uri="https://**************elastic-cloud.com:9243"
requireAuth="true"
username="**********"
password="******************">
<!--略-->

Dotnet Core AOP

用 AOP 的方式作成 Audit Log 記錄,
想法是方法的 in / out 我將想知道資訊記錄下來。
比如輸入的參數或是回傳值。
題外話,因為 AOP 的特性,如果方法處理到一半想要記錄是作不到的,
這是不是意味著必須重構將方法一分為二 ?

參考這篇,我會使用最基本的 Filter 實作 AOP,
Filter 簡介
依據 Filter 的特性我在這裡會實作 IActionFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AuditLogAttribute : Attribute, IActionFilter
{
private readonly ILogger logger;

public AuditLogAttribute(ILogger<AuditLogAttribute> logger)
{
this.logger = logger;
}

public void OnActionExecuted(ActionExecutedContext context)
{
this.logger.LogInformation("Result Filter End");
}

public void OnActionExecuting(ActionExecutingContext context)
{
this.logger.LogInformation("Result Filter Start");
}
}

我的最終目標是透過掛載 Attribute 的方式來讓 Logger 與解耦,
參考下方的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

[AuditLog]
public Result MyMethod(Context ctx)
{
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

這裡就有一件討厭的事,因為我的 Logger 都是透過建構子注入產生,
而自已本身也比較傾向不要使用公開 Property Injection 的方式*。
但是使用建構子在這裡會產生另一個問題,
我將無法使用掛載 Attribute 的方式處理 Logger
解決的方式是透過另一個 Attribute ServiceFilterAttribute 作間接掛載

1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

[ServiceFilter(typeof(AuditLogAttribute))]
public Result MyMethod(Context ctx)
{
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

這樣作還是有一些缺點,最明顯就是掛載的 Attribute 變長,
然後視覺上又是末端文字才能表達意涵,
另一點是原本約定成俗可以省略的*Attribute後綴不能省略了。

我想使用 CastleAutoFac 重作一次。
應該可以變得更加簡潔。

參考

(fin)

[實作筆記] Github 結合 Stryker 作變異測試

前情提要

前年初次接觸變異測試,去年在看重構時偷換了概念,
將兩者結合了。

這次我想更進一步,將 CI Server 與之結合。

Stryker.Net

跟據Stryker Handbook
Stryker 主要有三個專案,

  • Stryker (Javascript & TypeScript)
  • Stryker4s (Scala)
  • Stryker.NET (.NET)

我會使用我的練習用專案作為目標,
所以很理所當然的我會選擇 Stryker.NET,那我們就開始吧。

開始

安裝 Stryker

1
dotnet tool install -g dotnet-stryker

執行 Stryker

1
dotnet stryker -tp "['./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj','./test/Marsen.NetCore.Dojo.Integration.Tests/Marsen.NetCore.Dojo.Integration.Tests.csproj']" -p="Marsen.NetCore.Dojo.csproj" -dk=$STRYKER_DASHBOARD_API_KEY

注意幾個 cli 參數,
-tp 明確指定測試的專案有哪些,可以用中括號[]傳入多個測試專案名稱,使用,作為分隔符
-p 專案名稱
-dk dashboard-api-key 這組 key 是用來與 https://dashboard.stryker-mutator.io/ 互動的,
最主的功能是將報告上傳。
$STRYKER_DASHBOARD_API_KEY 是 Github 的 Secrets

你可以執行 dotnet stryker -h 查看更多原始說明

1
2
3
4
-tp|--test-projects Specify what test projects should run on the project under test.
-p|--project-file <projectFileName> Used for matching the project references when finding the project to
mutate. Example: "ExampleProject.csproj"
-dk|--dashboard-api-key <api-key> Api key for dashboard reporter. You can get your key here: https://dashboard.stryker-mutator.io

專案設定

新增一個 stryker-config.json
對我來說,最重要的是要記得設定 stryker-config > reporters > dashboard 這筆資料。
詳細的說明的可以參考這篇

1
2
3
4
5
6
7
8
9
10
{
"stryker-config": {
"dashboard-project": "github.com/marsen/Marsen.NetCore.Dojo",
"dashboard-version": "master",
"reporters": [
"json",
"dashboard"
]
}
}

在 Github 上工作

首先你必須在 Dashboard中 Enable Repository

Enable Repository

然後產生一組 Key

產生一組 Key

接下來到 Github , 我們將這組 Key 設定到 Secrets 之中

將 Key 設定至 Github

再到 Github Actions 中,把 workflow 指令設定完成,

Github Actions

觸發 CI 完成後,就可以在 Dashboard 上看到報表啦。

結果呈現

參考

(fin)

[實作筆記] Github 結合 SonarCloud 作代碼質量檢查 - 測試覆蓋率篇

前情提要

上篇,
我已經將 SonarCloud 的代碼檢查與 Github Action 結合在一起了。
透過專案首頁的 Dashboard 與專案的 Budget 我可以知道目前專案的一些狀況,
壞味道、技術債等 …

題外話,測試分類

在我的專案中有兩種測試,單元測試與整合測試。
我的分類方法,單一類別的 public 方法就用單元測試包覆。
不同的類別之如果有組合的交互行為,就用整合測試包覆。

舉個簡單的例子,以購物流程來說,我有的類別如下 :

  • Cart(購物車)
    • Add (加入商品)
    • Substract (移除商品)
    • CheckOut (結帳)
  • Order (訂單)
    • Caculate (計算結帳金額)

另外有兩種類別如下,

  • ECoupon
    • Caculate
  • Promotion
    • Caculate

上面的所有方法我都會加上單元測試作保護,
但是 Order 在呼叫 Caculate 的時候,
會以不同的順序組合 ECoupon.Caculate 與 Promotion.Caculate,
這個時候就有可能會產生不同的結果。

執行測試

同上一篇,整體的流程我們只要在 Build 完後加上測試即可。

  1. Begin
  2. MSBuild
  3. Test
  4. End

開發環境執行測試

1
dotnet test ./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj

產生測試報告

1
dotnet test ./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj --logger:trx;LogFileName=result.trx

測試報告會產生一份 result.trx 檔,在測試專案目錄底下的 TestResults 資料夾裡。
如果要在 SonarCloud 上使用請設定 sonar.cs.vstest.reportsPaths (VSTest適用),更多資訊請參考

result.trx

1
2
3
4
dotnet sonarscanner begin /k:"marsen_Marsen.NetCore.Dojo" /o:"marsen-github" /d:"sonar.host.url=https://sonarcloud.io" /d:"sonar.login="$SONAR_LOGIN
dotnet build ".\Marsen.NetCore.Dojo.Integration.Test.sln"
dotnet test ./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj --logger:trx;LogFileName=result.trx
dotnet sonarscanner end /d:"sonar.login="$SONAR_LOGIN

覆蓋率

  1. Begin
  2. MSBuild
  3. Test -產生報告
  4. Test -覆蓋率
  5. End

這裡實作上很簡單,但是在選擇上有一些困難與試誤,稍微作個記錄。

  1. dotconver (棄選)

    • 似乎要綁定 resharper 的 lincese
    • 有 30 天的限制,不知道會不會影響功能
    • 不知道怎麼用 commandline 下載執行程式至 Github Action 執行實體上。
  2. vstest.console.exe (棄選)

    • 似乎只能在 windows 上執行
    • 可以透過參數開始功能 --collect:"Code Coverage",但產生的 .cover 檔 SonarCloud 不支援需要轉換格式
    • 不知道如何將 .cover 轉換為 .coverxml , 可能是 visual studio enter prise 才有的功能 ?
  3. opencover (coverlet)

    • /p:CoverletOutputFormat=opencover /p:CoverletOutput=./coverage/ 的語法不 Work
    • --collect:"XPlat Code Coverage" 是比較新的參數用法,可以使用設定檔

因諸多原因,我最後選擇了 Opencover(Coverlet) 的作法,
記錄步驟如下,

首先要在測試專案上安裝 Nuget 套件 coverlet.collector
然後執行以下語法產生覆蓋率報告。

1
dotnet test ./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj --settings coverage.xml

coverage.xml 設定檔如下,這個檔案必須是 xml 檔,檔名並沒有限制 :
Configuration > Format 請記得填寫 opencover

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>opencover</Format>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>

最後至 SonarCloud 上設定 sonar.cs.opencover.reportsPaths 的路徑

result.trx

最後的最後,就來個 Budget 大集合吧

Bugs
Code Smells
Coverage
Duplicated Lines (%)
Lines of Code
Maintainability Rating
Quality Gate Status
Reliability Rating
Security Rating
Technical Debt
Vulnerabilities
SonarCloud
Quality gate

參考

(fin)

[實作筆記] Github 結合 SonarCloud 作代碼質量檢查

前情提要

大概一年前我曾寫過一篇 Blog [實作筆記] 讓 SonarQube 檢查你的代碼
沒什麼含金量,只是我個人用來記錄的筆記。
當初有一些問題沒有排除,加上工作一忙就沒有後續了。

我的理想目標是,每當我上 Code 到線上 Repo 時(Github),
SonarCloud 可以幫我檢查代碼,跑跑測試覆蓋率,刷新一下 Budget,
如果有異常(覆蓋率下降、壞味道等…)最好再發個通知給我。
這些功能要怎麼作到呢 ?

然後我會實際用在我的SideProject上,
這個 Project 單純只是為了練習而生,
專注於我個人的測試項目,主要語言為 Csharp 也有一些 TypeScript 。

分析

首先先排序一下優先序吧。

  1. 一定要可以執行代碼檢查
  2. 要能結合 CI ,我以 Github Action 作為我的主要 CI 工具
  3. 能夠跑測試並輸出測試報告
  4. Buget 刷新
  5. 發通知

這篇主要會說明如何結合 CI 執行代碼檢查。
執行代碼檢查就發現有一個問題, SonarCloud 一次只能對一種語言作檢查,
雖然我的專案裡有兩種語言,但是以 C# 佔大宗(93%),所以調整一下目標,
先優先完成 C# 的代碼檢查、結合 CI 與輸出測試報告。
之後再進行 Typescript 的檢查與測試,最後通知有的話很棒,沒有也沒關係啦 :)。

本機執行代碼檢查

我本機的環境有兩個,一個是 Windows 一個 macOS,我這裡只討論 Windows 的作法,
然後我就要直上 CI 了,Github Action 我並不熟悉,但是我知道上面應該是執行 Linux like 的作業系統。

首先要在 SonarCloud 上建立 Project ,
可以參考 Get started with GitHub.com 快速建立。

Administrator > Analysis Method

Analysis Methodn

這裡要把 SonarCloud Automatic Analysis 的功能關掉。
SonarCloud 支援自動分析語言只有以下

ABAP, Apex, CSS, Flex, Go, HTML, JS, Kotlin, PHP, Python, Ruby, Scala, Swift, TypeScript, XML.

雖然有很多,但可惜並沒有 C# ,所以要先關掉,不然 Github Action 執行時會收到下面的錯誤。

You are running CI analysis while Automatic Analysis is enabled. Please consider disabling one or the other.

另外目前支援的 CI 服務有 Circle CI 與 Travis CI ,
一樣殘念的是沒有支援 Github Action 。
另外兩個選項目是 Other CI 與 Manunlly(手動) 。
我的前一篇文章就是使用手動的方式把檢查報告打到 SonarCloud。
雖然只隔一年,但 UI 介面上已經有些差距,我還是再作一次介紹。

Analysis Methodn

首先先下載 SonarScanner,選擇正確的語言(Others)與OS(Windows)後下載,
接著設定環境變數

Setting Path

最後開啟 CMD 切換到專案目錄底下後。
執行語法,如果照著上述步驟,你可以在 Download 的按鈕下方找到語法,同時它會幫你填好 Token。

Begin

1
dotnet sonarscanner begin /k:"$ProjectKey" /o:"$Oragnization" /d:"sonar.host.url=https://sonarcloud.io" /d:"sonar.login="$Sonar_Login

MSBuild

1
dotnet build ".\Marsen.NetCore.Dojo.Integration.Test.sln"

End

1
dotnet sonarscanner end /d:"sonar.login="$Sonar_Login

$ProjectKey$Oragnization 這兩個變數可以在 SonarCloud 的 Overview 介面的右下角找到,
$Sonar_Login 則可以透過 Security 設定。

執行命名完成後,大概幾秒內就可以在 SonarCloud 中看到結果了。

簡單總結一下

  1. 你要有 SonarCloud
  2. 要下載 SonarSanner
  3. 依序執行 Begin > MSBuild > End

另外有一些雷包,在這裡也記錄一下

  • 要安裝 Java (Java8)
  • 執行語法的目錄底下不能有sonar-project.properties
    • 不然會報錯 (sonar-project.properties files are not understood by the SonarScanner for MSBuild.)
    • 我覺得應該是我的檔案內容有誤,但是還不知道怎麼修正。總之直接移除對我來說是可以 work 的。

CI 執行代碼檢查

同上面的概念,只要讓你的 CI Server 在執行的過程中依序執行 Begin > MSBuild > End 即可,
參考 Github Action 的 yaml 檔

1
2
3
4
5
6
7
8
9
10
11
12
#上略
- name: Install Dotnet Sonarscanner
run: dotnet tool install --global dotnet-sonarscanner --version 4.8.0
- name: SonarScanner Begin
run: dotnet sonarscanner begin /k:"marsen_Marsen.NetCore.Dojo" /o:"marsen-github" /d:"sonar.host.url=https://sonarcloud.io" /d:"sonar.login="$SONAR_LOGIN
- name: Build with dotnet
run: dotnet build ".\Marsen.NetCore.Dojo.Integration.Test.sln"
- name: SonarScanner End
run: dotnet sonarscanner end /d:"sonar.login="$SONAR_LOGIN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_LOGIN: ${{ secrets.SONAR_LOGIN }}

這裡要注意的是,
首先每次你都需要安裝 Dotnet Sonarscanner ,
其實我不清楚 Github Action 背後的機制,但是我猜測應該是用到容器化的技術,
每次 CI 執行時都會起一個實體(這個可設定,但是 Linux Like 的 OS 又快又便宜,就別考慮 Windows了吧參考)
所以每次都要重頭安裝相關的軟體,比如 : Dotnet Sonarscanner 。

另外一點是,環境變數的設定,可以看到最後面的 env 變數。
這個是機制是將 CI 的設定傳到實體的環境變數之中。
在 yaml 中綁定要使用 $+變數名 EX: SONAR_LOGIN
其它的變數,你可以在 Github 的 Secrets 頁面設定。
另外 Github 有些預設的變數
更多資訊可以參考

設定成功後,每次進 Code 就能看到代碼的壞味道、重複或是資安風險等資訊囉。
可以參觀一下我的專案

Quality gate

參考

(fin)

[實作筆記] 使用 IIS 作為 Reverse Proxy Server

情境

  1. 作業系統為 Windows 10
  2. IIS 的版本為 10.0.18362.1
  3. 前端使用 nodejs 開發了一個網站,會在 localhost:3000 執行,提供 UI
  4. 使用 dotnet core 開發了一個網站,在 IIS 上執行,用來提供 Api
  5. 透過鎖 Host 的方式 dotnet core 的網站綁定在 http://dev.api.test

目標

  1. 即使只有開發階段,我也不想看到 localhost:3000 作為我的網址
  2. 我想看到 http://dev.site.test 作為我的站台

本文

首先,鎖 Host 127.0.0.1 dev.site.test
Host 的檔案路徑為 C:\Windows\System32\drivers\etc

接下來請下載並安裝 URL RewriteApplication Request Routing

IIS 建立網站,繫結我設定為 dev.site.test:80
應用程式集區我沒有特別處。
到 IIS 選取站台的 Url Rewrite 新增規則
到 IIS 選取站台的 Url Rewrite 新增規則
選取 Reverse Proxy 規則

選取 Reverse Proxy 規則

填寫 localhost:3000

填寫 `localhost:3000`

這個時候前往 dev.site.test 就可以看到站台囉。

參考

(fin)

[實作筆記] 從 TDD 到 TDD ,Todo 到 Test 趨動開發(二)

上篇

異常處理的 Todo 項目

異常處理有幾種狀況,
一種是回傳的狀態有異常,
一種是回傳的資料有異常,
最後一種是超乎預期的異常,
比如說 Http 通訊上本身有問題。

再進一步分析這三種狀況,
我會寫下以下幾種情境

  1. 回傳的狀態有異常,記錄回傳的異常狀態,拋出 Exception
  2. 回傳的資料有異常,記錄回傳的異常資料,回傳空資料
  3. 超乎預期的異常,記錄異常資料, 拋出 Exception

測試案例 回傳的狀態有異常,記錄回傳的異常狀態,拋出 Exception

新增測試案例
這裡複製之前的測試案例,
再透過 inline Method 還原 arrange 部份的代碼,
再修改成我們想要的測試案例。

這裡我們先驗証拋出 Exception

1
2
3
4
5
6
7
8
9
10
11
12
[Fact]
public void Case6_Query_Error_Result()
{
_configService.GetAppSetting("pickup.service.url")
.Returns(UrlMockResultError);
_storeSettingService.GetValue(_testStoreId, "pickup.service", "loginId").Returns("testId");
_storeSettingService.GetValue(_testStoreId, "pickup.service", "auth").Returns("testAuth");
target = new PickupService(_configService, _storeSettingService);

Action act = () => target.GetUpdateStatus(_testStoreId, _testWaybillNo);
act.Should().Throw<Exception>();
}

Production Code 就單純很多了

1
2
3
4
if (obj.result == "error")
{
throw new Exception();
}

下一步我要驗証記錄 Log 的行為
出錯的時候應該呼叫 LogError 的方法

原本想直接驗証 LogError 有沒有被呼叫

1
2
3
4
5
6
7
[Fact]
public void Case7_Query_Error_Result_Should_LogError()
{
GetPickupServiceWith(UrlMockResultError);
target.GetUpdateStatus(_testStoreId, _testWaybillNo);
_logger.Received().LogError(Arg.Any<string>());
}

因為這裡會拋出 Exception ,
所以無法直接呼叫 GetUpdateStatus
要修改前一個測試

1
2
3
4
5
6
7
8
[Fact]
public void Case6_Query_Error_Result()
{
GetPickupServiceWith(UrlMockResultError);
Action act = () => target.GetUpdateStatus(_testStoreId, _testWaybillNo);
act.Should().Throw<Exception>();
_logger.ReceivedWithAnyArgs().LogError(default(string));
}

而 Produciton Code 很單純的加上 Logger 並調整建構子

1
2
3
4
5
6
7
8
9
10
11
12
13
-   public PickupService(IConfigService configService, IStoreSettingService storeSettingService)
+ public PickupService(IConfigService configService, IStoreSettingService storeSettingService, ILogger logger)
{
this._configService = configService;
this._storeSettingService = storeSettingService;
+ this._logger = logger;
}

if (obj.result == "error")
{
+ this._logger.LogError(obj.result);
throw new Exception();
}

回傳的資料有異常,記錄回傳的異常資料,回傳空資料

測試案例

1
2
3
4
5
6
7
[Fact]
public void Case7_Query_Error_Content()
{
GetPickupServiceWith(UrlMockContentError);
var actual = target.GetUpdateStatus(_testStoreId, _testWaybillNo);
actual.Should().BeEmpty();
}

調整代碼以通過測試

1
2
3
4
5
6
7
+    if (string.IsNullOrEmpty(c.ErrorCode))
+ {
switch (c.Status)
{
////略…
}
+ }

超乎預期的異常,記錄異常資料, 拋出 Exception

測試

1
2
3
4
5
6
7
8
[Fact]
public void Case8_Query_Exception()
{
GetPickupServiceWith(UrlMockException);
Action act = () => target.GetUpdateStatus(_testStoreId, _testWaybillNo);
act.Should().Throw<Exception>();
_logger.ReceivedWithAnyArgs().LogError(default(string));
}

Production Code 就直接整個用 try Catch 包起來再記 Log

實務上的案例

這裡補充一些實務上的情境,

  1. 呼叫狀態查詢時,對方的 API 只允許同查詢 100 筆 WayBillNo
  2. 呼叫 API 後多了幾種文件外的狀態需要處理
    • D → Finish
    • F → Finish
    • E → Abnormal

單元測試現身

現在我已經有一些整合測試作保護了,
但是想要修改或重構仍然很麻煩,
原因是我每次有新的情境就需要準備新的 Mock API(實務上我需要準備符合情境的 WayBillNo),
透過 Todo 與整合測試,已經讓我們的代碼有了雛型。
在一切太晚之前,我們需撰寫單元測試。

Do TODO 建立單元測試

這裡小小提個 Visual Studio 2019 的小問題 ,
預設只會安裝 MSTest 的 Generator ,
這裡我要安裝 XUnit 的 Generator
安裝完成後再透過 Code Generator 產生第一個測試,紅燈。

當然這種 Generator 產生的 Code 不是實際要的測試案例
調整一下測試案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ILogger logger = Substitute.For<ILogger>();
IStoreSettingService storeSettingService = Substitute.For<IStoreSettingService>();
storeSettingService.GetValue(Arg.Any<long>(), "pickup.service", "auth").Returns("FakeAuth");
IConfigService configService = Substitute.For<IConfigService>();
configService.GetAppSetting("pickup.service.url").Returns("https://test.com/");

var target = new PickupService(configService, storeSettingService, logger);

var actual = target.GetUpdateStatus(2, new List<string> {"TestWayBillNo"});
actual.Should().BeEquivalentTo(new List<ShippingOrderUpdateEntity>
{
new ShippingOrderUpdateEntity
{
AcceptTime = new DateTime(2020, 03, 03, 17, 51, 20),
OuterCode = "TestWayBillNo",
Status = StatusEnum.Finish
}
});

Legacy Code 相依 HttpClient

大部份的功能我都可以透過 DI 的手段隔離,
但是之前的 Test Driven Develop 的方法並沒有將 HttpClient 轉換成可以隔離的物件。
另外一部份代碼是透過 Copy Paste 手法產生的代碼,所以也有可能會有 Legacy Code。
這裡我優先處理 HttpClient 。

首先我要重構一小段代碼,所幸之前的整合測試可以保護我這段重構

1
2
3
4
5
6
7
8
+       internal HttpClient HttpClient;
private const string DeliveryOrder = "DeliveryOrder";

try
{
var result = new List<ShippingOrderUpdateEntity>();
- var httpClient = new HttpClient();
+ this.HttpClient?? = new HttpClient();

在測試的保護下,我要逐步修改我的 HttpClient ,
好讓我的單元測能夠通過。
其實我目前的單元測試還未完成,所以可以先 Skip 掉,
等 HttpClient 隔離完成後再回頭完成單元測試。

隔離 HttpClient

這裡我要回顧一下,之前在作 Kata_Api_Pay 的時候,
我在 Production Code 建立了 IHttpClient 的介面,
用於隔離 HttpClient
我可以延用 HttpClient 但是因為我未實作 DefaultRequestHeaders 欄位,
這會導致一些錯誤;
雖然我可以一併調整但是這樣我要同時面對兩份遺留代碼,
我認為這樣的風險太大,而且使用 IHttpClient 目前看起來出現一些問題。

  1. 雖然抽出介面,但依賴在 HttpClient 之上,未來有功能不足或未實作 HttpClient 的功能就仍需要調整。
  2. 最初的目的其實是為了隔離,而隔離的目的是為了好測試,這些代碼卻放在 Production Code 上實在很奇怪。

基於以上種種理由,我要重新作一次隔離。
要達到幾個目地。

  1. 真正的與 HttpClient 解耦,未來再有用到 HttpClient 的任何方法/欄位皆不影響即有代碼。
  2. 將這類的工具放到正確專案 TestingToolkit 之下,不再影響 Production Code

首先允許測試專案存取 Production Code 的 Internal 欄位

1
2
+       [assembly: InternalsVisibleTo("Marsen.NetCore.Dojo.Tests")]
namespace Marsen.NetCore.Dojo.Kata_PickupService

下一步,偽造 HttpClient 的回傳值,
我們可以透過 HttpClient 的建構子作到這件事。
參考這篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
target.HttpClient =
new HttpClient(
new MockHttpMessageHandler(JsonSerializer.Serialize(
new ResponseEntity
{
Result = "",
Content = new List<Content>
{
new Content
{
ErrorCode = string.Empty,
Status = Status.DONE,
lastStatusDate = "2020-03-03",
lastStatusTime = "17:51:20"
}
}
}),
HttpStatusCode.OK));

偽造 HttpClient 的回傳值後,我就可以把單元測試的部份完成,
案例蠻多的,但是大同小異也沒有什麼特別的技法,
就不多贅述。

稍微提一下,反而在寫 Unit Test 過程中,
發現了 Production Code 一些 Over Design 的代碼。

比如說,多餘的邏輯分支,在某些因果條件,跟本不可能被執行到的代碼。
我視作無用的代碼將他移除。

另外也有發現一些 Entity 在呼叫 API 的過程不會取用它的資料或欄位,
也許有得人會想要移除這些 Entity ,但我會傾向保留,
原因是這些 Entity 是在整合測試階段被趨動出來的,
雖然沒有用到而且會使代碼的覆蓋率下降,
但是我認為這些代碼很有可能再下一個階段就會被用到,
在不影響功能的情況我不會刻意移除。

整體而言,測試已 100% 覆蓋,
也記錄了如何從 Todo Driven 到 TDD 的想法與技巧。
最後整理一下代碼,
把 MockHttpMessageHandler 搬到 TestingToolkit。
最後回頭把 api Pay 對 HttpClient 的處理調整一下就大功告成啦。

參考

(fin)