2022年8月7日

程式是人寫出來的,測試也是

本文章首次刊登於 Web 長島計畫《第三期:測試,這不是測試

軟體測試是什麼

軟體開發的過程中,除了功能本身的開發外,還有一個很重要的環節是「測試」。有的人聽到「寫測試」,直覺反應是「好麻煩」,或覺得這只是個浪費時間的事。如果程式一開始就寫的好,為什麼還需要花時間來寫測試呢?
當然,如果能確定程式是「正確無誤的」,就可以省去寫測試的時間。然而,實際情況下,程式是人寫出來的;是人寫的,就有機會犯錯,只是錯的多或少罷了。
如果把整個「軟體開發」的過程比喻做「寫考卷」,開發者就像是寫考卷的學生。不管你有沒有讀書,都還是可以作答並交出考卷。至於要怎麼知道答案寫的正不正確、能夠得到多少分數,就得要靠「對答案」才能知道了。「對答案」這個動作,其實就是「測試」在做的事。測試是在核對開發者開發出來的功能,是不是確實符合 PM 撰寫的產品規格。
相信讀者一定有過這樣的經驗:我們認為這題答案一定是 A,但解答卻是 D。寫程式也是一樣,你可能會認為,程式這樣寫應該就「對了」。然而,程式執行後到底會不會有問題,還是得要做了「測試」才知道。
實際上,平常在開發功能的過程中,開發者就一直在做測試了。
假設你正在開發一個提示視窗。你預期使用者點了問號的按鈕後,這個視窗就會彈出在畫面上。於是,在開發好這個功能後,你會實際去點這個按鈕。點下去之後,如果這個提示視窗也真的有跳出來,你才會認為,你成功完成這個功能了。在這個前端的例子中,你「預期」點了按鈕應該要有反應,且程式執行後,也「確實」得到你預期的結果。後端的例子可以是這樣的:在開發好 API 後,你會透過工具來實際看看發出請求後的「實際結果」,確認這些結果是否和你的「預期結果」相同。
這個比較「預期結果」和「實際結果」是否相同的過程,就是測試的核心概念。這裡的一個重點是,如果連你自己也不知道預期的正確結果是什麼,那根本沒辦法開始進行測試
如果連你自己也不知道預期的正確結果是什麼,那根本沒辦法開始進行測試

用程式測試程式:寫測試

既然說「比較『預期結果』和『實際結果』是否相同的過程」就已經是在進行測試,為什麼我們還需要額外花時間寫測試呢?其中最大的差別就在於「手動」和「自動」。
開發者之所以要「寫測試」,就是通過程式的方式,把手動測試這個繁瑣的流程給自動化。如此,就可以在每次產品上線前,讓程式自動跑一次完整的測試,而不需要手動測試每一項功能。剩下來的時間,不論是要用來開發新功能、外出買一杯珍珠奶茶、或是轉頭和同事聊聊天,都要比每次重複手動執行測試開心得多。
除此之外,測試檔本身就是一份帶有記錄的文件。開發者可以寫下當初開發時沒留意到的情境,當成測試案例,避免未來有其他開發者重蹈覆轍。
接著就讓我們來看一下,通常測試實際是怎麼寫的。

實際的測試範例

讓我們以 JavaScript 的 Jest 為例來看一段實際測試的程式碼。
假設我們寫了一個名為 sum 的函式,這個函式應該要把參數的值相加後回傳。如果 sum 這個函式帶入的參數是 1 和 2,應該預期可以得到 3。這時候針對 sum 這個函式,就可以寫出如下的測試:
it('sums numbers', () => {
  const actualResult = sum(1, 2);
  const expectedResult = 3;

  expect(actualResult).toEqual(expectedResult);
});
即使你不會寫 JavaScript,從變數的命名,應該也可以猜想這段程式執行的內容:
  • 這裡有一個名為 sum 的函式,我們把這個函式執行的結果存成變數 actualResult
  • 接著把預期的正確結果存成變數 expectedResult
  • 最後透過 expect 這個方法,來比較「實際執行的結果(actualResult)」是不是和預期的結果(expectedResult)相同(toEqual
上面這個過程就是最基本測試的概念。透過程式來測試寫好的程式,將可以不用再手動透過 console.log() 的方式把結果列印出,再比對原本寫的 sum 有沒有錯。以上這些動作,都可以透過程式自動幫我們進行檢測。
如果測試中預期結果和函式實際執行後的結果相同時,就會得到 PASS:
Screen Shot 2021-12-20 at 12.34.10 AM
相反的,如果 sum 這個函式的實作邏輯有錯誤,sum(1, 2) 得到的並不是 3 時,就會顯示 FAIL:
Screen Shot 2021-12-20 at 12.36.16 AM
這裡測試的結果告訴開發者,雖然我們預期會得到 3,但實際上得到的是 0,所以測試並不通過。
這裡雖然是以 JavaScript 來舉例,但實際上,測試的核心概念在不同語言間是通用的,都是去比較「預期的正確結果」和「程式執行後的實際結果」是否相同來判斷。
例如,以 Go 語言來說:
func TestAdd(t *testing.T) {
	actualResult := Sum(1, 2)
	expectedResult := 3

	if actualResult != expectedResult {
		t.Fail()
	}
}
同樣會看到需要去比較 actualResult 是否和 expectedResult 相同,如果不同的話,該測試就會 Fail。
如果讀者有在 LeetCode 刷題的經驗,實際上,當你提交答案時,背後運行的邏輯就和執行測試是一樣的。它已經預先列好了預期的正確結果(expectedResult),然後用你答案中所寫的函式去實際執行,看看得到的實際結果(actualResult)和預期的正確結果是否相同,來判斷你有沒有通過測試。這些測試,都可以透過程式實際執行,而不需要人工額外核對。

測試的類型

你可能好奇,透過上面這種方法,就可以針對各種不同情境,像是網頁 UI、API 的 request-response 進行測試嗎?
答案是肯定的。根據測試的對象和範疇不同,可以分成幾種不同類型的測試。像是上面的例子中,我們針對單一 sum 這個函式所進行的測試,一般我們就稱作「單元測試(unit testing)」。另一方面,如果是需要多個函式共同運作才能達到的測試,例如測試某個前端框架中的元件(component)、或是測試一個 API 回應的資料是否正確這類的,則會稱作「整合測試(integration testing)」。還有一種甚至使用程式的方式,來模擬使用者實際操作網頁的行為,就好像真的有一個使用者在操作網頁一般,看整個網站的功能能否正常運作;這種類型的測試則稱作「端對端測試(End to End Testing)」。
但不論是上面那一種,核心的概念都還是一樣的:
  • 都是透過程式,而不是人工手動,來測試原本的程式邏輯是否正確
  • 開發者都需要先有一個預期正確的結果,再拿實際執行後的結果,和預期的結果做比對

一定要寫測試嗎

答案是「不一定」。更精確來說,答案會隨時空背景而有不同。
撰寫測試的好處,除了確保「目前」程式的穩定性外,還有一個重點:我們想要省去「未來」重複測試時,所需的人力和時間成本。寫測試絕對是個好習慣,但寫測試的當下,就需要額外的時間和人力;如果未來程式邏輯有改動,測試也勢必需要再跟著調整。
所以,如果你只是初步有個想法,想要實作看看其他人使用的反應,未來程式邏輯改動的幅度還很大;又或者只是寫個簡單的小工具,未來也不會再添加新功能,我會認為,你未必需要在一開始就補齊測試。甚至,就算假設你的專案已經稍有規模,然而你的公司目前採取的商業模式,是以開發新功能以確保新客戶簽約為主,你也未必需要把有限的資源,投入在補齊測試的撰寫上。
要特別留意的重點是,測試這件事情絕對不是全有全無;不是說你要寫測試,就一定每隻程式都要寫到。相較於完全不寫測試,或者一寫就全部的程式都要有測試,或許可以先針對你有疑慮、或是規格比較確定的程式來撰寫測試。筆者認為,相較全有全無的做法,這樣的策略比較有彈性,而且才能夠走得長久。

測試不是考試分數,寫愈多不代表程式品質就越好

有些入門的開發者,在認識了測試的概念後,會誤以為測試寫越多,程式的品質就一定越好、bug 也一定會越少。因此,她們花了非常多的時間和篇幅在寫測試。然而,千萬切記,測試本身也程式,就和開發功能時所寫出來的程式一樣,絕對不是越多越好。比起亂槍打鳥,寫了一大堆不可能發生的測試案例,更好的策略,是用更精簡、清楚的方式,來完成預期的測試項目。
有時,甚至會看到「球員兼裁判」的荒謬情況,直接把程式執行的實際結果,當成「預期的正確結果」來進行測試。這種荒謬的測試方式,欠缺對於程式正確結果的思考。舉例來說,我們知道 sum(1, 2) 要得到 3,但現在這個程式卻回傳了 0。然而,如果寫測試的開發者沒有去思考正確的答案應該要是 3,而是直接拿 0 當作預期的結果,就會寫出這樣的測試:
it('sums numbers', () => {
  const actualResult = sum(1, 2); // 0
  const expectedResult = 0; // 直接拿 sum (1, 2) 的結果當作預期的正確結果

  expect(actualResult).toEqual(expectedResult);
});
這時候測試確實會 PASS 通過,且測試的覆蓋率也有提升,但這樣的測試有意義嗎?
寫測試和軟體開發一樣,都需要思考和確保程式品質,絕對不是越多越好。在網路上,也有許多文章在說明如何寫出有品質的測試。有興趣的讀者可再根據自己所使用的程式語言,去查找相關的 best practice 或 guideline。

測試的實踐

由於撰寫測試的確需要花上額外的時間,所以在許多公司中,會有品質保證團隊(Quality Assurance Team)。QA 團隊會整理和思考各種可能的測試情境,並且根據需求,撰寫不同類型的測試(大多是端對端測試)。另外,也會再搭配手動和自動化測試,確保產品在上線後能良好運作。
除了公司中有專職的 QA 團隊,來進行軟體測試外,還有一種開發流程稱作「測試驅動開發(Test-Driven Development,TDD)」。前面我們有提到,測試的核心概念就是去比較預期結果和實際結果。一般來說,我們會先把功能開發完後,才去寫測試來檢驗開發的功能是否符合預期,但 TDD 的流程則相反。
在 TDD 中會先撰寫測試,也就是先定義好預期的正確結果是什麼。不過,由於還沒有實際開發這個功能,所以可以預期的,一開始測試執行的結果一定是失敗的。然而,因為已經把預期的結果都定義好了,所以開發者接著要做的,就是開始實作這些功能,把原先失敗的測試,最終都轉變為成功後才算是完成開發。透過 TDD,能夠讓開發者養成先思考再實作的習慣,先釐清功能的需求、規格和介面後才能開始實作。
這篇文章中,說明了撰寫測試的許多好處,但要特別留意的是:透過測試的撰寫,可以有效避免程式可能的錯誤,但並不能保證程式一定沒有問題。畢竟,使用者實際的使用環境,往往比測試時複雜許多。除了 App 本身的穩定度之外,還會受限於 App 執行的環境,例如作業系統、網路速度、硬體規格等等。甚至,使用者的操作複雜且意想不到,也都可能導致意料之外的問題產生。這也就是為什麼,有些軟體升級後,馬上就會在推出一版更新(patch)來修正問題--因為實際的環境,往往比測試複雜更多。不過,這並不是不寫測試的理由。畢竟,儘管寫測試也許不能讓你的軟體完全沒問題,但不做測試的軟體,絕對有更高的機會,連正常的流程都走不完就壞了。
透過測試的撰寫,可以有效避免程式可能的錯誤,但並不能保證程式一定沒有問題

2021年8月30日

在 Windows 上配對 Apple 鍵盤時需要輸入 PIN 碼(Apple keyboard PIN)

keywords: Mac 鍵盤PIN 碼Magic KeyboardWindows巧控鍵盤
最近在把 Magic Keyboard 和 Windows 筆電配對時發生一件很奇怪的事,就是當我點擊「連結」時,它一直要求我「輸入『鍵盤』的 PIN」,但重點是,螢幕上沒有跳出任何告知我鍵盤 PIN 碼的畫面,我也不知道鍵盤的 PIN 碼到底什麼是什麼。
後來找到了這篇文章(How do I pair apple wireless keyboard with windows 10),和底下留言的人說的一樣,這個解法非常荒謬,但它真的有效!
後來我發現,原來在 Windows 配對 Magic Keyboard 時,重點是配對時需要在電腦上先隨意輸入一組 PIN 碼,接著再用 Apple 的鍵盤輸入同樣的 PIN 碼後按 Enter 就可以了,像是這樣:
  1. 先讓你的鍵盤可以被電腦搜尋到,然後在電腦上點擊「新增藍芽或其他裝置」
  2. 選擇「藍芽」找到鍵盤後,點擊「連結」
  3. 這時候它會要求你「輸入『鍵盤』的 PIN」,這時候用「電腦原本的鍵盤」輸入任意一組數字(例如,12345678)
  4. 輸入完後用滑鼠點擊「連結」,電腦會好像在等待什麼一樣
  5. 接著在「Apple 鍵盤」上輸入和剛剛輸入相同的 PIN 碼,然後按下鍵盤的 Enter
邏輯上,這樣應該就搞定了。

2021年8月14日

認識 TypeScript 中的型別魔術師:Mapped Type

TypeScript Mapped Type
在 TypeScript 中,Mapped Type 是一個非常有趣的東西,一開始可能會覺得有點抽象,但一旦熟悉了它的概念後,Mapped Type 就像是「型別魔法師」一樣,可以根據型別組合出各種不同的型別,也就是說如果想要能夠靈活建立 Type 的話,Mapped Type 是一個一定要會的概念。

Index Signatures Type

在了解 Mapped Type 之前,需要先來看一下它的前身 Index Signatures Type。一般來說,在 TypeScript 裡定義物件的型別會需要把物件的每一個 key 和 value 的型別都定義清楚,像是這樣:
type Person = {
  firstName: string;
  lastName: string;
  age: number;
};
但有些時候,因為一些原因,也許是 key 的名稱不是那麼重要時,或者 key 的可能太多事,我們可以使用 index signature type 來定義這個物件,例如,定義一個 key 為 string,value 則為 stringnumber 的型別:
type PersonDict = {
  // "key" 可以是取成任何名稱
  [key: string]: string | number;
};
[key: string] 中的這個 key 可以是任何名稱,你也可以改成 [property: string] 效果是一樣的。

Mapped Type 和 in operator

在了解 index signatures type 後,就可以來初步認識 Mapped Type。而在 Mapped Type 中最重要的就是 in 這個關鍵字的使用。
先來看看 in 怎麼用:
type PersonMap = {
  [key in 'firstName' | 'lastName']: string;
};
這裡和 index signatures 類似,你一樣會看到像是 [key: ... ] 這樣的寫法,key 一樣是可以自己取的變數名稱,而不一樣的是多了 in 這個關鍵字。
這個 in 的感覺非常「類似」在 JavaScript 中 Array 的 for...of 方法,上面的 [key in 'firstName' | 'lastName'] 可以想成是這樣的感覺:
// Mapped Type 中的 [key in 'firstName' | 'lastName'],很類似於 for...of 的方法
for (const key of ['firstName', 'lastName']) {
  console.log(key);
}
for (const key of ...) 中,這個 key 會是每次疊代時陣列取得的元素值,所以這裡的話第一次會是 firstName,第二次會是 lastName
回到 Mapped Type 中的 [key in 'firstName' | 'lastName'],這裡的 key 也是很類似的概念,可以想像成會跑一個迴圈,第一次 key 的值會是 firstName,第二次的值則是 lastName
也就是說,上面的:
type PersonMap = {
  [key in 'firstName' | 'lastName']: string;
};
其實就是:
type PersonMap = {
  firstName: string;
  lastName: string;
};
Tip:
把 Mapped Type 中的 key in ... 想成是類似 for (const key of ...) 的概念,是我認為理解 mapped type 最重要的一步。它都有跑一個迴圈把所有元素依序取出來的概念。
由於 key 只是一個變數名稱,它也可以命名成其他名稱,又因為它表示的是物件屬性(property),所以也很常會用 P 來表示它,例如下面建立另一個 mapped type:
type Device = {
  [P in 'apple' | 'samsung' | 'google']: string;
};
如果了解剛剛的說明的話,應該可以想到,這裡 P 就會依序是 applesamsungpixel,出來的型別會等同於:
type Device = {
  apple: string;
  samsung: string;
  pixel: string;
};
這裡你也可以理解到 mapped type 和 indexed signature 的差別,mapped type 可以視為是 indexed signatures 的子集合(subset),它能將物件的屬性定義的更明確,而不是單純用某個型別來表示:
// index signatures:物件的屬性只要是 string 即可
type DeviceDict = {
  [key: string]: string;
};

// mapped type:物件的屬性需要是 'apple' | 'samsung' | 'google'
type DeviceMap = {
  [P in 'apple' | 'samsung' | 'google']: string;
};
單純看到這裡,可能還是感受不到為什麼會說 Mapped Type 是可以用來操作型別的「型別魔術師」,只會覺得 Mapped Type 單純只是可以「跑回圈」用的。
要讓 Mapped Type 發揮它強大的功力前,還需要了解另外一個重要的東西是 Keyof Type Operator

Keyof Type Operator

keyof 的作用其實很直觀,就是把物件的 key 給取出來:
type Person = {
  firstName: string;
  lastName: string;
  age: number;
  isMarried: boolean;
};

type PersonKeys = keyof Person;
基本上這個 PersonKeys 就會是所有 Person 物件 Key 的聯集,這裡就是 firstName | lastName | age | isMarried
TypeScript keyof
Tip:
在 TypeScript 中,object key 的型別可能是 stringnumbersymbol,所以如果使用 keyof any 得到的型別就會是 string | number | symbol

Mapped Type 搭配 keyof 的組合技

再了解 Mapped Type 和 Keyof Operator Type 之後,就可以使用這兩個的組合技。

範例一

假設現在我們定義了一系列的事件:
type SupportedEvent = {
  click: string;
  change: string;
  keyup: string;
  keydown: string;
};
這時候如果想根據 SupportedEvent 中屬性的名稱,產生一個新的型別叫做 HandledEvent,但物件 value 的型別要全部換成 function 的話,我們當然可以如同過去一個一個把屬性定義出來:
type HandledEvent = {
  click: () => void;
  change: () => void;
  keyup: () => void;
  keydown: () => void;
};
但這麼做除了很麻煩之外,未來如果有新增支援的事件類型到 SupportedEvent 的話,還需要同時記得加到 HandledEvent 這個型別中,如果忘記加的話,兩個型別中支援的 Event 類型就會不一致。
這時候使用 Mapped Type 搭配 keyof 就會非常的方便:
type HandledEvent = {
  [K in keyof SupportedEvent]: () => void;
};
寫成這樣就搞定了,未來如果 SupportedEvent 中有新增的事件類型是,HandledEvent 也不需要額外改動。
現在來了解一下我們剛剛的組合技是怎麼使用的。
首先要注意到的是 [K in keyof SupportedEvent]
  • 前面有提到 K 只是一個變數名稱,會對應到的是 in 後面每次取出來的值。
接著把注意力放到 in 後面的內容,它是 keyof SupportedEvent
  • 搭配前面對於 keyof 的理解,可以知道 keyof SupportedEvent,對應到也就會是 click | change | keyup | keydown
也就是說如果換成 JavaScript for ... of 的方法,它會像是這樣:
//  [K in keyof SupportedEvent]
for (const K of ['click', 'change', 'keyup', 'keydown']) {
  /* ... */
}
這樣你就可以知道,K 其實對應到的就會是 clickchangekeyupkeydown
再來是 [K in keyof SupportedEvent]: () => void;: 後的內容就會是物件 value 的型別。因為我們想要把它改成 function,所以就在後面放了 () => void

範例二

讓我們來看另一個 Mapped Typed + keyof 的例子,假設我們現在定義了一個 PersonMethod,它的 value 可能有很多不同的型別:
type PersonMethod = {
  greet: (name: string) => string;
  age: number;
  isMarried: boolean;
  name: string;
};
這時候需要定義一個 PersonMethodOptions 的型別,它的屬性名稱會和 PersonMethod 相同,但 value 的型別都是 boolean,用來表示要不要開關這個功能,這時候如果不會使用 Mapped Type 的話,我們可能會一個一個將屬性重複定出:
type PersonMethodOptions = {
  greet: boolean;
  age: boolean;
  isMarried: boolean;
  name: boolean;
};
如同前一個範例一樣,一旦 PersonMethod 中有添加新的方法,你就需要把這個方法也記得添加到 PersonMethodOptions,否則就會有不一致的情況。
但如果會使用 Mapped Type 搭配 keyof 的話,只要這樣就可以搞定了:
type PersonMethodOptions = {
  [P in keyof PersonMethod]: boolean;
};
至於為什麼這樣寫就可以,可以按照前一個範例的說明,試著思考看看。

P 不只是變數

在剛剛上面的範例中,我們分別使用了:
type HandledEvent = {
  [K in keyof SupportedEvent]: () => void;
};

type PersonMethodOptions = {
  [P in keyof PersonMethod]: boolean;
};
不論是 KP 有說到它都只是個變數名稱,要取名成什麼都可以,但實際上它也不單單只是個名稱,它還可以被拿來使用,舉例來說,我們可以把它當成 : 後的型別,像是這樣:
type HandledEvent = {
  // : 後面使用 K
  [K in keyof SupportedEvent]: K;
};

type PersonMethodOptions = {
  // : 後面使用 P
  [P in keyof PersonMethod]: P;
};
至於對應出來的型別結果會是什麼,大家可以自己嘗試看看,相信會可以加深你對 Mapped Type in 這個關鍵字的理解。

進階:使用 as 把物件 key 的名稱也做轉換

剛剛上面的範例中,我們大部分是保留原本物件的屬性名稱,只針對物件 value 的型別作轉換,但實際上,我們甚至也可以去轉換物件 key 的名稱。這裡會用到 TypeScript 中 Template Literal Typesas 的概念。
Template Literal Types 可以簡單想成就是 JavaScript 中的 template literal,也就是可以透過反引號(`)來在字串中帶入變數。
as 則可以讓我們在 Mapped Type 中改變物件 key 的名稱。
以上面我們用過 SupportedEvent 當作範例:
type SupportedEvent = {
  click: string;
  change: string;
  keyup: string;
  keydown: string;
};
在 React 中,很常會使用 handleXXX 來作為方法的命名,這時候如果想要根據 SupportedEvent 的屬性 key ,但產生的是新的 keyName,變成都是以 handle 當作前綴時,除了可以自己一個一個打,像下面這樣之外:
type HandleEvent = {
  handleClick: () => void;
  handleChange: () => void;
  handleKeyup: () => void;
  handleKeydown: () => void;
};
也可以利用 Mapped Type 的觀念,這樣寫就可以了:
type HandleEvent = {
  [P in keyof SupportedEvent as `handle${Capitalize<P>}`]: () => void;
};
如此也不用擔心未來 SupportedEvent 用新的 Type 加進去卻忘了補進 HandleEvent 的情況。
要理解上面的語法,一樣可以一步一步來,首先知道 P 是一個變數名稱,它可以拿到的是每次疊代時 keyof 後的內容,接著 as 的意思是我要把這個物件的 key 換一個名稱,換成的名稱是 `handle${Capitalize<P>}`
首先因為 P in keyof SupportedEvent ,所以你會知道 P 其實就是 clickchangekeyupkeydown
as 後面則是我們希望的新名稱是什麼,如果我們只是寫 `handle${P}` 的話,轉出來的 key name 會是 handleclickhandlechangehandlekeyuphandlekeydown,但因為我們希望 handle 後的第一個字母要大寫,所以這個範例中有用了 TypeScript 內建的 Capitalize 這個 type utility,它的作用是幫我們把 P 的第一個字變成大寫,於是轉出來的 key 名稱就會是我們想要的 handleClickhandleChange、...。

後記

這裡提到的只是 Mapped Type 搭配 keyof 做出的變化,Mapped Type 也很常會搭配 Indexed Access TypeCondition Types 做出更多的組合應用,這也就是為什麼 Mapped Type 可以稱作是「型別魔法師」的重要地位了。
另外,上面提到的寫法也多可以透過泛型(Generic)抽成自己的 utility type 來做重複的應用。
未來有機會的話會在來分享 Mapped Type 搭配其他組合技做出的更多應用。

範例程式碼

最後附上這篇文章中有用到的 Code 供大家參考。

參考資料

2021年7月4日

透過工具建立有規範的 git commit message 吧

過去曾經整理過如何透過 semantic-release 這套工具整合 CI/CD 來達到自動化更新套件版號、產生 CHANGELOG 檔,並發佈到 npm 的流程。
當時雖然有提到 conventional commit 這個撰寫 commit message 的規範(convention),但還沒有實際透過一些好用的工具來建立 commit message 和針對 commit message 進行檢查。
在這篇文章中就來介紹幾套用來建立符合 conventional commit 的好用工具:
  • 透過 commitlint 進行 commit message 的檢查(lint)
  • 搭配 husky 在建立 commit message 前就自動執行 commitlint
  • 透過 commitizen 方便開發者建立符合 conventional commit 的 commit message
  • 使用 conventional-changelog 根據 commit message 來產生 CHANGELOG 檔
  • 使用 standard version 來同時更新版本號和產生 CHANGELOG 檔
如果是對於 CI/CD 中如何自動更新版號的部分,則可以參考過去寫的系列文章:發佈 npm 套件 - 從手動到自動

commitlint:檢查 commit message

commitlint 這套工具是用來作為 git commit 的 linter,並且可以搭配不同的 convention。
這裡選擇 config-conventional,也就是需要依據 conventional commit 的規範來寫 commit message:
  • @commitlint/cli 是用來執行 commitlint 的工具
  • @commitlint/config-conventional 是根據 conventional commit 所建立的規範

初次安裝

# 安裝 commitlint-cli 和 config-conventional
npm install --save-dev @commitlint/{config-conventional,cli}

# 會在專案中建立 commitlint.config.js 並放入設定
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

使用

# 使用 commitlint
$ echo "add commitlint" | npx commitlint
> ✖   subject may not be empty [subject-empty]
>type may not be empty [type-empty]
如果這個 commit message 不符合規範的話,會跳出錯誤:
commitlint

搭配 husky

但和 eslint 類似,如果不能在建立 git commit message 時就自動檢查規則的話,這個工具就會變得有點冗,這時候可以搭配 husky 這套工具。
# 第一次安裝 husky 才需要執行
$ npx husky-init && npm install

# 建立 commitlint 用的 git hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
husky 這套工具可以讓開發者在不同的 git hook 執行不同的動作,例如在建立 commit 前(pre-commit)執行 ESLint 的檢查,如果檢查沒過就不能建立該次 commit。
這裡則是利用 husky 在 commit-msg 這個 git hook 去檢查 commit message 有沒有符合 conventional commit 的規範。這時打開專案中的 .husky/commit-msg,應該會長這樣:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx --no-install commitlint --edit $1
預設執行 npx husky-init && npm install 的指令後,husky 會自動在 .husky 資料夾中建立 pre-commit 的檔案,它會告訴 husky 在建立 git commit 前去執行 npm test 的指令。但如果是 create-react-app 的專案,因為預設 npm test 會執行的是 watch mode,因此需要改成 npm test -- --watchAll=false
這時候在建立 commit message 是就會自動使用 commitlint 進行檢查,如果不符合規範的話,就無法成功建立 commit:
commitlint with husky

commitizen:建立 commit message

除了可以用 commitlint 搭配 husky 來檢查 commit message 之外,再來很重要的就是要能夠簡單方便的建立符合 conventional commit 的 message。因此就有幫助開發者建立 conventional commit message 的好用工具。
其中 commitizen 是同事 Ken 推薦非常多人使用的工具,它用起來的體驗蠻不錯的。另一套則是 commitlint 本身提供的 @commitlint/prompt-cli

commitizen(較推)

commitizen 是非常多人使用的的工具,相較於 @commitlint/prompt-cli 用起來感覺更友善一些:
# 安裝
npx commitizen init cz-conventional-changelog --save-dev --save-exact

# 之後要建立 commit 的話,只需要執行
npx cz
commitizen

在 global 使用 commitizen

如果你不想要把每個專案都透過 npx commitizen init 變成 commitizen-friendly 的專案,想要可以直接在全域下使用 commitizen,可參考 Conventional commit messages as a global utility 這麼做:
npm install -g commitizen  # install commitizen globally
npm install -g cz-conventional-changelog # Install commitizen adapter globally

# create `.czrc` in home directory
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

# 建立 git commit 時,只需要使用
$ git cz

@commitlint/prompt-cli

除了可以使用 commitizen 外,也可以使用 @commitlint/prompt-cli 來輔助我們建立 git commit message,但基本上挑一套使用就可以了。
# 初次安裝
npm install --save-dev @commitlint/prompt-cli
安裝完後執行:
# 建立 commit message
npx commit
就會跳出對應的 CLI 工具來協助建立 git commit:
@commitlint/prompt-cli

conventional-changelog:建立 CHANGELOG

在根據 conventional commit 來寫 commit message 後,我們還可以自動產生對應的 CHANGELOG 檔。這裡則會使用 conventional-changelog-cli
只需要執行:
  • -p angular:如果有使用 conventional commit 來建立 commit message 的話,就可以加上此選項。它會用符合 conventional message 的內容來產生 CHANGELOG;如果沒加此參數的話,所有 git message 都會進到 changelog 中。
# 第一次安裝
$ npm install --save-dev conventional-changelog-cli

# 檢視可以使用的 options 和說明
$ npx conventional-changelog --help

# 第一次產生 CHANGELOG
npx conventional-changelog -p angular -i CHANGELOG.md -s -r 0

# 將新的更新 message 添加到 CHANGELOG
npx conventional-changelog -p angular -i CHANGELOG.md -s
也可以把產生 CHANGELOG 的指令放到 package.jsonscripts 中:
diff --git a/package.json b/package.json
index 18f0899..37a1d41 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
  "scripts": {
     "build": "react-scripts build",
     "test": "react-scripts test",
     "eject": "react-scripts eject",
+    "changelog": "conventional-changelog -i CHANGELOG.md -s,
     "lint:staged": "lint-staged",
     "prepare": "husky install"
   },
@@ -51,6 +52,7 @@
  "devDependencies": {
     "@commitlint/cli": "^12.1.4",
     "@commitlint/config-conventional": "^12.1.4",
+    "conventional-changelog-cli": "^2.1.1",
     "cz-conventional-changelog": "^3.3.0",
     "husky": "^7.0.0",
     "lint-staged": "^11.0.0"
之後就只需要執行 npm run changelog 就會產生最新的 CHANGELOG 檔。

standard version:更新套件版本號

最後這套 standard version 是在針對套件進行版號更新,並同時產生該次更新的 CHANGELOG 檔,也就是說不需要再額外使用上述 conventional-changelog 的工具。
如果你產生 CHANGELOG 的時間都是在更新版本號時,且是使用 conventional commit 在建立 commit message 的話,則可以用 standard version 就好,不用再使用 conventional-changelog。
它會:
  1. 根據 conventional commit 的內容,依據 semver 的原則來更新版號
  2. 產生對應的 CHANGELOG
# 檢視所有可用的指令
npx standard-version --help

# 第一次 release
npx standard-version --first-release

# 更新套件版號和 CHANGELOG
npx standard-version

# 透過 dry-run 先看看會有什麼改變
npx standard-version --dry-run

# 更新到指定的版本
npx standard-version --release-as minor # 指定更新 minor 的版號
npx standard-version --release-as 1.1.0 # 指定更新後的版號
需要特別留意的是,當版號還在 v0.y.z,major 還沒進到 v1 是,會被視為是 pre-production 的產品,因此 feature 和 fix 都只會更新 patch version;breaking change 則只會更新 minor version。可以參考這個 issue 的討論:Version bumping isn't working properly based on Commit types
另外有需要的話,也可以把這個工具加到 package.json 中的 scripts 中:
diff --git a/package.json b/package.json
index d18c992..9832508 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "test": "react-scripts test",
     "eject": "react-scripts eject",
     "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
+    "release": "standard-version",
     "lint:staged": "lint-staged",
     "prepare": "husky install"
   },
執行的話變成:
# 更新版本
npm run release

# 執行 dry-run 看實際執行的話會有什麼改變
npm run release -- --dry-run

說明影片

2021年5月4日

從找衣服了解時間複雜度(Time Complexity)

剛剛用日常上班前挑衣服的例子和沒學過程式的 00 說明時間複雜度的概念很好理解耶~!
例子是這樣的...
一早要出門的時候,想要從衣櫃中找出紅色的上衣。
其中一種方式是像左圖一樣,這是掏寶上很熱門的「疊衣服褲子收納神器」,雖然看起來整理的很乾淨,但如果你要從中找到紅色的衣服,你就得要由上而下一件一件找,最糟的情況就是一直翻到最下面才能找到你要的紅色衣服。
另一種方式是像右圖一樣,把衣服用立起來的方式,一眼就可以看到紅色的衣服在哪,直接拿出來,幾乎不用找。
左圖的那種方式,時間複雜的就是 O(n),n 就是衣服的件數,雖然紅色的衣服有可能就放在最上面,一眼就可以看到,但在探討時間複雜度的時候都要考慮最差的情況,所以如果你有 n 件衣服,最差的情況就是要把 n 件衣服都翻過才會找到紅色那件。
右圖的方式它的時間複雜度是 O(1),在你沒有忘記其實衣服已經被丟到洗衣籃的前提下,你看一眼,翻都不用翻就可以把紅衣服直接取出(請先忽略掉人腦內建的視覺搜尋系統,那是另一個有趣的故事 XD)。這種不用一個一個找,就直接取出的,時間複雜度就是 O(1)
有了這個時間複雜度的概念後,是不是覺得左邊的那個商品實用性沒這麼高啦~ XDD
但我還是附一下購物連結(誤)
真的是沒想到學演算法還可以用在購物吧!

圖片來源

2021年4月26日

[心得] 2021 求職面試心得分享

由於過去求職時在 ptt 上或許多個人網誌中獲得了許多幫助,因此這次也來分享自己面試的心得,希望對於求職的大家們能夠有些幫助。
這次求職過程中,在和幾位不同的 Team Lead 或是 CTO 面談的過程中,真的讓我感受到多數厲害的人總是自信而謙虛的,他們不會透過問題來讓你覺得自己不懂,反倒是很 open-minded 讓你感受到雖然這個自己現在不懂,但沒關係,甚至會進一步透過提問來協助你進一步釐清自己的思路。
同樣地,我也期許對於自己的專業能夠是「自信而謙虛」的態度。
過去雖然常聽大神說,工作一陣子後,通常就不用自己找工作,而是靠別人介紹或挖角,但我可能還沒到這個階段,周圍沒什麼人介紹,更別說是挖角,所以還是只能靠自己 XDD。
下面是我這次有面試的幾間公司,主要找公司市場不侷限於台灣的公司,面試的職缺全部都是前端工程師。

Line Pay

首先 Line Pay 和 Line Taiwan、Line Bank 雖然都隸屬於 Line 集團底下,但在台灣是三間不同的公司。Line Pay 的面試過程較嚴謹,這次從投遞履歷到最終回覆的時間約需要一個多月的時間。
Line Pay 公司的地點是在大直美福大飯店的側邊,給人非常氣派豪華的感覺。

第零關:線上測試

給予連結,並透過線上測試的方式,題目主要是演算法的測驗,印象中是三題。

第一關:onsite-interview with Taiwan Engineer

面試的對象是 Taiwan Line Pay 的工程師們,會針對線上測試的作答進行討論,接著會根據過去實作過的專案進行問答,並可利用白板進行概念的解釋與說明。

第二關:onsite-interview with Korean Engineer

這關給我的經驗很特別,因為是透過視訊的方式和韓國的工程師們進行面試,原本以為會需要用英文會話,但面談現場直接就有一位中韓文的即時口譯,所以並不需要說到英文,和面試官的溝通會完全透過這位翻譯。(心裡 OS:大公司就是直接找翻譯這樣的氣派。)

第三關:onsite-interview with HR

最後會和 HR 進行面談,除了討論期待的薪資,也會針對個人或過去的工作經驗進行暸解。據 HR 表示,目前 Line 和 Line Bank 都搬到同一棟建築物,但 Line Pay 因為剛搬到美福大飯店這邊不久,因此暫時沒有再次搬遷的打算。
最後,HR 會與你進行基本的英文會話,確認有基本英文溝通能力。

KKStream

KKStream 則是隸屬於 KKBOX Group 的公司,做的是影音串流服務,可以想成是讓客戶能夠透過 KKStream 的服務建立 Netflix 或 MyVideo 這類影音平台。

第零關:線上測試

給予連結,並透過線上測試的方式,題目主要是演算法的測驗。

第一關:onsite-interview with Team Lead

完成線上測驗通過後,會有一份作業,作業內容是用 React 寫一個待有基本的 CRUD 以及搜尋功能的網站(可以簡單想成 TodoList 這類),作業需在此次面試前完成,並提交到 Github 私人的 repo。
KKStream 前端目前分成三個組別-Core Tech、BlendVision 和 Enterprise。各組各派一人前來面試,會針對作業內容進行討論,接著則根據過去開發過的專案進行討論。

第二關:onsite-interview with PM & Engineer Manager

第一關結束後,會根據各組人數的需求將面試者配到適合的組別,也就是一開始投的組別,不見得會是最後的組別,這個部分也可以再和 HR 或 Engineer Manager 進行了解。
PM 和 Engineer Manager 比較不是針對技術的部分進行發問,而是針對過去的經驗試著了解自己是個怎麼樣的人。在這次面試中和 Engineer Manager 聊了蠻久的時間,包括帶領 Team 的方式、對於前後端的想法、測試撰寫的想法等等,覺得有非常多的收穫。

第三關:online-interview

HR 會針對期待薪資進行了解,並試著了解自己過去的經歷。另外,會與一位主管進行面談,過程比較像是在聊天,互相分享彼此的經驗和價值觀。

OneDegree

OneDegree 的前端工程師還有分不同 team,分別是做 2C 和 2B,這裡我是面試 2B 的團隊。OneDegree 主要是開發保險系統,讓保險公司能透過此保險系統建立保險商品,並供一般消費者能夠以網路進行線上投保。

第零關:線上測試

給予連結,並透過線上測試的方式,題目包含演算法、React 和 Git 的問答題。

第一關:onsite-interview with Team Lead

主要是與 Frontend Team Lead 們進行面試,一開始會先請面試者以英文自我介紹,並且透過英文進行簡短的問答,主要也是確認面試者有基本的英文能力。接著會切換回中文,同樣是根據過去做的專案進行討論,並且分享彼此對於不同技術上的想法。

第二關:onsite-interview with Taiwan Director & HR

再來會與 OneDegree 台灣區的總監和 HR 進行面談,這次面談比較不會談到技術上的問題,比較像是互相了解彼此的聊天。

總結

OneDegree 的回應速度還蠻快的,對於面試者來說不會有太長時間的等待。

Privé Technologies

Privé Technologies 的 recruiter 主動聯繫,Privé Technologies 是一間立基於香港的 Fintech,工程師遍佈在世界不同的地方,目前工程師主力是在香港和台灣。

第零關:線上測試

給予連結,並透過線上測試的方式,題目主要是前端 JavaScript / React 的測試。

第一關:online-interview with Frontend Team Lead

透過 Online 的方式與位在香港的 Frontend Team Lead 進行面談,過程全程使用英語。主要是針對過去的專案進行提問,也有詢問到對撰寫測試的想法,並討論「怎麼樣算是好的程式碼」?

第二關:online-interview with Frontend Engineer

一樣是透過 online 的方式進行面談,面試官是在台灣的前端工程師(很巧的是過去和他還有短期合作過相同的專案)。一開始一樣會以英文進行自我介紹和簡短的問答,確認面試者有足夠的英文會話能力。接著會切換回中文,討論「怎麼樣算是好的程式碼」、還有聊到 OOP 和 Functional Programming 適合的時機、另外則是 JavaScript 有關的題目。
另外,也有進行 online 的 coding test,內容偏向基本的演算法和邏輯實作。

第三關:online-interview with CTO

印象沒錯的話 CTO 是澳洲人,但在香港待了蠻久的一段時間,目前和 Frontend Team Lead 一樣都在香港,他也曾在 LaLaMove 擔任過 CTO 的職位。
面試全程以英文進行,一樣會討論到「怎麼樣算是好的程式碼」,另外則是聊一些個人的經歷、和同事的相處、人格特質等等的。

總結

Privé Technologies 是回覆相當快速的公司,收到回覆後會立即安排下一場的時間,不論是 HR、Team Lead 到 CTO 都給人很和善親切的態度,可以讓人感受到是相當尊重且重視面試者的。

慧科訊業 Wisers

慧科是由 Headhunter 推薦,公司主要是做輿情分析的,會去爬各媒體或社群的資料、關鍵字,以此分析當前熱門的議題或輿論風向,市場主要是在中國。

第一關:online-interview

第一關會先以線上的方式進行面談,面試官來自台灣、香港和中國。這裡我有一點小烏龍的是,收到通知的時候,看到 email 寫的是「phone interview」,誤以為是面試官會打電話來...,接著等了又等,想說時間到了怎麼都沒打電話來,後來才知道 phone interview 指的是視訊面談 XDD
面試主要問過去實作過哪些專案,有沒有處理過複雜的圖表或大量資料需要 render 的經驗,怎麼樣優化和除錯等等。

第二關:onsite-interview

雖然說是 onsite-interview,但除了和台灣的工程師面試之外,同時也會透過視訊和香港以及中國的主管面談。中國的 Frontend 主管問了很多技術相關的問題,從 CSSOM、Web Component、micro-frontend、performance、如何避免瀏覽器被阻塞(block)都有問到。

總結

可以感覺的出來 Frontend Team Lead 的知識深度很深,我也並沒有全部都回答得出來,但 Team Lead 人非常有耐心,就像個 mentor 一樣,會給我一些思路讓我再去思考這個問題有沒有其他的可能性或答案,面試完後真的有一點「如沐春風」的感覺不誇張 XD。

常見問題

如果需要準備英文面試的話,也很推薦 Coursera 上這堂免費的課程 English for Career Development,若時間不夠的話,也可以根據自己的需要,直接跳著進度看,不用從頭慢慢看。
從技術面來說,JavaScript 面試的內容除了可以參考很久以前整理過的「JavaScript: Understanding the Weird Part(JavaScript 全攻略:克服JS 的奇怪部分)」筆記之外,網路上也可以搜尋到非常非常多;React 的話,基本的官方文件一定要看過。
有些東西雖然每天都在用,但若沒準備一時被問到的話,還是可能會沒辦法很快速且順暢的解釋出來。例如,請你解釋 event loop 是什麼。
在上述的面試中,撇除技術問題外,有一些共同常被問的,像是:
  • 自我介紹
  • 為什麼離開前一份工作
  • 上一份工作中覺得最困難或最具挑戰的是什麼?
  • 想像未來三年後你預期自己會是個什麼樣的人?
  • 有什麼問題要問我的嗎?
另外,也可以參考這部 3 分鐘的影片,裡面許多題目大家一定都聽過,但還是可以稍微想一下怎麼回答:

2021年4月20日

[心得] ALPHA Camp X 天下雜誌:那些年我們一起走過的數位轉型

ALPHACamp X 天下雜誌:那些年我們一起走過的數位轉型
這次很幸運有機會能夠擔任 ALPHA Camp 與天下雜誌合作舉辦的實體/線上活動,也是我自己第一次擔任活動主持人。
從小,我家就非常多與天下相關的雜誌,不論是天下、康健、Cheers 都有,因為我爸媽算是很早期的訂戶,所以一開始收到 ALPHA Camp 詢問能否的擔任活動主持人時,我內心是有些興奮的,是一種竟然可以有機會進到從小就一直在接觸的媒體的雀躍感,而且天下雜誌多數時候也給我相當正面的感受,算是台灣媒體界的清流之一。
除了天下雜誌相當知名的媒體之外,我也很好奇工程師在當今的媒體產業中究竟扮演了什麼角色。主力是在開發「內容管理系統」?或多在進行資料視覺化的專案?
在這次的活動中很幸運能夠聽到已經在天下雜誌 10 多年的資深產品經理-紹謙,和數位轉型過程中加入團隊,負責從專案發想到實際落地的資深工程經理-世彥,一起來聽他們分享天下雜誌的數位轉型。
ALPHACamp X 天下雜誌:那些年我們一起走過的數位轉型

數位轉型是什麼?

在聽演講前,其實我完全不知道「數位轉型」究竟是怎麼一回事,我單純地以為就是把紙本書變成電子書,或是推出幾個 App 後,就算是完成了數位轉型?

數位轉型不是事件的完成,而是持續且沒有終點的歷程

然而,在聽完兩位講者的分享後,我深切感受到數位轉型絕對不是「做完了 OO」就叫完成了數位轉型這麼簡單,並非把紙本雜誌電子化、推出幾個手機 App、開發幾個 Web 專案就叫完成了數位轉型。數位轉型的過程相當複雜,牽涉到的不只是使用到的技術、文章撰寫的工具、還包括整個組織架構的調整。
最早天下雜誌完全是紙本作業、編輯是以寫稿紙作業。過去上稿流程是先有紙本文章然後把文章變成電子檔後放在網路上,現在則轉變成線上文章先推出後,才有實際紙本內容;過去團隊中大部分都是編輯,現在則多了產品經理、軟體工程師、數據分析師等各種角色加入團隊;過去沒辦法從使用者的操作中取得大量的資料,現在則可以透過使用者使用 App 或閱讀文章等資料作為反饋,來協助指導後續的決策和產品修正。

數位轉型是不怕面對改變,敢於挑戰不熟悉的事物

如果要我說數位轉型是什麼?我認爲就是不怕潮流的變化,努力學習讓自己能夠跟上最新的事物,不怕面對自己不熟悉的事物,願意不斷嘗試與接受挑戰。這就好像一個永遠不會老的人,不斷地透過新陳代謝讓自己的身體和腦袋維持在年輕的狀態。
天下雜誌並沒有在完成了雜誌電子化,推出手機 App、建立網路內容平台後,就因為覺得「完成了數位轉型」而停下來。在近年 Youtube 的普遍和 Podcast 的竄起,天下雜誌也都沒有缺席,推出了 Youtube 頻道和 Podcast 節目,繼續跟著數位的潮流前進。因為數位轉型是一種不怕面對改變,敢於挑戰不熟悉事物的精神,因此它是沒有什麼叫做「已經完成的」

有價值的工程師

在這場活動中,兩位講者也分享到他們認為什麼樣的工程師是有價值的。

先釐清問題,再提出更多可能的解決方式

其中一點是當 PM 提出一個需求(或功能)時,好的工程師會試著向 PM 去了解這個需求(或功能)是想要解決什麼問題,先把實際想要解決的問題釐清,接著再和 PM 討論如果是想要解決這個問題的話,有哪些可行的做法,因為 PM 一開始提的功能或許只是其中一種解決方式,但工程師在釐清問題後,會多了工程面可行性的評估,進而有機會提出不同的解決方式供 PM 參考與選擇。

對於開發的產品有 Ownership

另外一個特質則是對產品有強烈的 Ownership,工程師不單純只是把 PM 所交付的功能完成,同時他也喜歡自己完成的產品、會好奇使用者使用時的體驗、會去思考怎麼樣能把這個產品做得更好。

不怕面對改變,勇於嘗試新事物、敢於挑戰自己的態度

作為軟體工程師,總是有新的技術、沒用過的工具、沒聽過的詞彙,但許多時候我也會怠惰,覺得學新的好累、現有的東西就夠用了吧?聽完這場活動後,我最大的反思在於,我想有價值的工程師絕對不止是學會了某些技術或工具後,就成為了一個優秀的工程師,反倒是一種態度或精神-一種不怕面對改變,勇於嘗試新事物的精神;一種不怕碰到沒解過的問題,敢於挑戰自己的態度,而這點也正好呼應了這場活動談到的「數位轉型」。

總結

很開心初次主持活動就能和天下雜誌合作,而且又能夠聽到兩位非常有經驗的產品經理與工程經理進行分享。
ALPHACamp X 天下雜誌:那些年我們一起走過的數位轉型
ALPHACamp X 天下雜誌:那些年我們一起走過的數位轉型