Working With Legacy Code
==========
一個系統中的業務流程,往往會與多個物件有所關聯.

```Controller的action handler依賴service物件, service又依賴repository物件, repostiory物件又依賴database.```
要做單元測試前,可能必須要先準備好這些所有物件,
然後把一群物件都放入測試當值中,但它們彼此依賴又環環相扣.
要測試handler, 卻事先要準備好service, service又需要先準備好repository, repository又需要db; 所以測試一個物件, 要先準備好除了自己之外的QQ
## 單元測試的前提
1. 快速的定位出錯誤點
2. 花費的時間夠快,需要的測試環境不需太複雜(不需要準備好DB, SMTP...)
## 感測 & 分離
### 感測Sensing
適用場景:
```We can't access values our code computes```
Solution:
```break dependencies to sense the value```
### 分離Separation
適用場景:
```we can't even get a piece of code into a test harness to run```
Solution:
```break dependencies to separate the code```
## Dependency-Breaking Techniques
解依賴的三個基本技巧

### 參數化方法Parameterize Method
```go=
func Method1() {
mResult := new(Result);
mReuslt.xxxx();
....
}
```
這樣子Method1高度耦合於Result物件, 我們無法感測到Result的值.
可以選擇```參數化方法```來讓Method1來感測到Result的值.
白話文, Result的值與內容是我們可以完全掌控的.
```go=
func Method1(mResult Result) {
mReuslt.xxxx();
....
}
```
### 參數調配Adapt Parameter
當無法對一個引述的型別使用介面提取, 或者該引述難以偽裝時.
```go=
type ARMDispatcher struct {
marketBinding []string
}
func NewARMDispatcher() *ARMDispatcher {
return &ARMDispatcher{
marketBinding = make([]string, 0)
}
}
//HttpsParameters 是原生的Http參數型別, 難以偽裝與測試;
// 且過程中還需要感測其值
func (a *ARMDispatcher) Populate(parameters HttpsParameters) {
var values []string = parameters.GetCipherSuites()
if values != nil && len(values) > 0 {
a.marketBinding = append(a.marketBinding, values[0])
}
}
```
1. 替Populate用到的HttpsParameters類型新增interface, 且簡單能表達意圖就好
2. 為新interface建立一個產品程式碼的實現類型
3. 為新interface建立一個測試程式碼的偽造類型
4. 寫一個簡單測試用例, 將偽裝物件傳給該目標方法
5. 修改原來方法使其能使用新的引數型別
6. 執行測試, 確保work
```go=
type ParameterSource interface {
GetParameterValue() string
}
type HttpParameterSource struct {
mHttpsParameters HttpsParameters
}
func NewHttpParameterSource(httpsParameters HttpsParameters) *HttpParameterSource {
return &HttpParameterSource{
mHttpsParameters = httpsParameters
}
}
func (p *ParameterSource) GetParameterValue() string {
var values []string = p.mHttpsParameters.GetParameterValue()
if values != nil && len(values) > 0 {
return values[0]
}
return "";
}
```
```go=
type ARMDispatcher struct {
marketBinding []string
}
func NewARMDispatcher() *ARMDispatcher {
return &ARMDispatcher{
marketBinding = make([]string, 0)
}
}
func (a *ARMDispatcher) Populate(parameterSource ParameterSource) {
var values string = parameters.GetParameterValue()
if values != nil {
a.marketBinding = append(a.marketBinding, values)
}
}
```
```go=
type FakeParameterSource struct {
values string
}
func NewFakeParameterSource(values string) *FakeParameterSource {
return &FakeParameterSource{
values = values
}
}
func (p *FakeParameterSource) GetParameterValue() string {
return values
}
func TestARMDispatcherPopulate(t *testing.T) {
armDispatcher := &ARMDispatcher{}
fakeParameterSource := NewFakeParameterSource("Hello World")
armDispatcher.Populate(fakeParameterSource)
assert.Equal(t,1, len(armDispatcher.marketBinding) )
assert.Equal(t, "Hello World", armDispatcher.marketBinding[0])
}
```
Adapt Parameter通常都會修改道原來的簽章, 為了把意圖通用化.
https://gunnarpeipman.com/refactoring-adapt-parameter/
### 介面提取Extract Interface
Extract Interface並沒有要一次提取依賴類別上所有Public methods.(記得SOLID中的ISP嗎)
可以考慮一步一步地提取出所需要的.
```go=
type PaydayTransaction struct {
transactionLog *TransactionLog
}
func NewPaydayTransaction(transactionLog *TransactionLog) *PaydayTransaction {
return &PaydayTransaction{
transactionLog : transactionLog
}
}
func (p *PaydayTransaction) Run() {
p.transactionLog.SaveTransaction()
}
type TransactionLog struct { }
func NewTransactionLog() *TransactionLog {
return &TransactionLog{}
}
func (p *TransactionLog) SaveTransaction() {
// call db
}
func (p *TransactionLog) RecordError(code int) {
// log error
}
```
1. 建立一個新interface, 取有意義的命名, 暫時別加入任何方法
2. 建立一個目標類別來實作該interface
3. 把想使用偽造物件的地方, 通通從原來類別,改成引用這新interface
4. 按下編譯, 編譯器會說缺少什麼方法, 再慢慢新增對應方法
```go=
type ITransactionRecorder interface {
SaveTransaction()
}
```
```go=
type PaydayTransaction struct {
transactionRecorder ITransactionRecorder
}
func NewPaydayTransaction(transactionRecorder ITransactionRecorder) *PaydayTransaction {
return &PaydayTransaction{
transactionRecorder : transactionRecorder
}
}
func (p *PaydayTransaction) Run() {
p.transactionRecorder.SaveTransaction()
}
```
```go=
type FakeTransactionLog struct {
IsSave bool
}
func NewFakeTransactionLog() ITransactionRecorder{
return &TransactionLog{
IsSave : false
}
}
func (p *FakeTransactionLog) SaveTransaction() {
// do something
p.IsSave = true
}
func (p *FakeTransactionLog) RecordError(code int) {
// log error
}
func TestPayday(t *testing.T) {
fakeTransactionLog := NewFakeTransactionLog()
paydayTransaction := NewPaydayTransaction(fakeTransactionLog)
paydayTransaction.Run()
assert.Equal(t,true, fakeTransactionLog.IsSave )
}
```
https://docs.microsoft.com/zh-tw/visualstudio/ide/reference/extract-interface?view=vs-2019
其他好用技巧
但不是物件導向語言未必能實作
### Extract and Override
### Pull Up Feature
1. Pull Up Field
2. Pull Up Method
3. Pull Up Constructor Body
### Push Down Dependency
1. Push Down Method
2. Push Down Field
https://refactoring.guru/push-down-method
https://faculty.csbsju.edu/jschnepf/CS230/Slides/Refactoring.pdf
# Test Doubles
Test doubles : 一個概念名詞, 描述測試中所有non-production ready的依賴物件.
讓依賴物件看起來的樣貌跟行為非常相似release-intended的版本, 但內容裡卻是簡化非常多的版本.
Test doubles有5種, 但大致區分成兩大類

- Stub : 協助模擬資料的交互與感測
- Mock : 有助於模擬與檢查, 其交互對象的內部狀態(呼叫次數,執行多久,非同步的執行狀態...)


[reference](https://enterprisecraftsmanship.com/posts/when-to-mock/)
[reference2](https://www.softwaretestingmagazine.com/knowledge/unit-testing-fakes-mocks-and-stubs/)