從數(shù)據(jù)流動角度解決測試難題

上篇講述了增強(qiáng)邏輯功能測試而改進(jìn)MVC為MVP,但是這樣做可能還不夠徹底,現(xiàn)在來討論另一個純粹從測試角度設(shè)計的框架。

首先我們來明確一下,測試中最核心的東西是什么。當(dāng)然是數(shù)據(jù),我們永遠(yuǎn)是圍繞著數(shù)據(jù)來的,那么之前一些架構(gòu)的問題是什么。無論哪個框架,數(shù)據(jù)的流通都是雙向的,當(dāng)數(shù)據(jù)流通成為單向了會怎么樣呢?

in data ==> Module ==> out data

這樣我們偽造數(shù)據(jù)進(jìn)行測試就會非常方便了。按照這個思想就有了數(shù)據(jù)單向流通的架構(gòu)。

數(shù)據(jù)單向流通的實(shí)現(xiàn)

這個概念最早是在web中提出的,應(yīng)用在React里,官方的方案是Redux。現(xiàn)在swift也提出了一種實(shí)現(xiàn)ReSwift

我在之前寫React的時候使用過這種方案,從開發(fā)角度來說,這種方案會大大增加開發(fā)難度,代碼量也會大量增加,而且開發(fā)思路也需要從以前的思考方式轉(zhuǎn)換過來。但是如果我們把這個思路轉(zhuǎn)換過來,其實(shí)對整個流程是更加簡化和分離的。

從測試角度看,我覺得無疑是我知道的最可測的一種框架,甚至可以測試部分視圖的邏輯。

那么總的來說,很難說這種結(jié)構(gòu)的好壞,就算不考慮增加的開發(fā)時間,也是一種難以給以一種評價的方案。

(Redux/ReSwift)框架介紹

方案的幾個核心是:

  • 數(shù)據(jù)的單向流通
  • 每個視圖都可以看做一個狀態(tài)機(jī)
  • pure function

關(guān)于pure function,我就不做太多介紹了,簡單的說,就是同一輸入必定會有相同的輸出,是非常容易測試的一種函數(shù)。

首先,我們來看一下官方的架構(gòu)圖。

可以看到,數(shù)據(jù)流動方向都是朝一個方向進(jìn)行的。那么下面從每個模塊來介紹下,還是以star button為例子。

State

視圖狀態(tài)機(jī),也是所有會更新界面數(shù)據(jù)保存的地方,可以認(rèn)為相當(dāng)于ViewModel。

首先我們star會有以下幾種視覺樣式

enum StarButtonState {
    case star
    case staring
    case unstar
    case unstaring
}

所以State可以定義為

struct StarState: StateType {
    var state: StarButtonState
    var starCount
}

Action

首先我們定義幾種狀態(tài)機(jī)轉(zhuǎn)換的Action類型

struct StarAction: Action { }
struct StaringAction: Action { }
struct UnstarAction: Action { }
struct UnstaringAction: Action { }

以及相應(yīng)的功能以及狀態(tài)變更,這里異步請求采用延遲來代表。

func star(id: String) -> Store<StarState>.ActionCreator {
    return { state, store in
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            store.dispatch(StarAction())
        }
        return StaringAction()
    }
}
func unstar(id: String) -> Store<StarState>.ActionCreator {
    return { state, store in
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            store.dispatch(UnstarAction())
        }
        return UnstaringAction()
    }
}

View

視圖層其實(shí)很簡單,只需要根據(jù)State的不同來更新就可以了。注意的是,更新都是無狀態(tài)的,和上一個狀態(tài)無關(guān),所以view層是個無狀態(tài)層。

class StarButton: UIButton, StoreSubscriber {

    let store = Store<StarState>(reducer: starReducer, state: nil)

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.store.subscribe(self)
    }

    func newState(state: StarState) {
        // update UI
    }
}

Reducer

狀態(tài)轉(zhuǎn)換器,唯一可以更新State的地方。

func starReducer(action: Action, state: StarState?) -> StarState {
    var state = state ?? StarState(state: .star, starCount: 0)

    switch action {
    case _ as StarAction:
        state.state = .star
        state.starCount += 1

    case _ as StaringAction:
        state.state = .staring

    case _ as UnstarAction:
        state.state = .unstar
        state.starCount -= 1

    case _ as UnstaringAction:
        state.state = .unstaring

    default:
        break
    }

    return state
}

數(shù)據(jù)傳遞

那么最重要的就是數(shù)據(jù)如何傳遞的了。首先要明確的是每個模塊能夠修改的,或者說是傳遞的,只能是下個模塊。

比如,用戶star button觸發(fā)了一個事件:

func onButton(sender: StarButton) {
    if (store.state.state == .unstar) {
        store.dispatch(star(id: id))
    }
    else if (store.state.state == .star) {
        store.dispatch(unstar(id: id))
    }
}

此時會創(chuàng)建Action,也就是將view事件轉(zhuǎn)換為Action。然后會傳遞到store中,store會調(diào)用Reducer進(jìn)行處理。Reducer更新state之后又會觸發(fā)store的subscribe事件,回到view的func newState(state: StarState)

View (User Event)
==(create)==> ActionCreator/Action
==(dispatch)==> Store <--(Update State)--> Reducer
                  \==(subscribe)==> View (newState)

大概的一個流程就是這樣了。

接下來說說這樣做的模塊化的優(yōu)勢。

模塊化和測試性

首先,我們需要有函數(shù)式編程的概念,函數(shù)也是一等公民,所以ActionCreatorReducer都是獨(dú)立的模塊。

作為使用者,我們在不需要像MVC一樣知道這些api所代表的操作功能,相對應(yīng)的,我們需要去了解一個模塊的動作(Action),比如以上例子就是

func star(id: String)
func unstar(id: String)

這樣的劃分比MVC要友好的多,真正的把邏輯功能從原本的C中分離開。需要觸發(fā)這個行為也非常簡單store.dispatch(star(id: id))。相比MVP,行為更加的獨(dú)立,每個行為之間完全沒有聯(lián)系,也不會產(chǎn)生干擾影響。同時因?yàn)槊總€行為的獨(dú)立性,可復(fù)用程度也就越高。

Reducer則代表了view層的更新,也可以非常明確的知道每個狀態(tài)的變更發(fā)生了什么。相比其他模式,將界面更新完全交給view或者Controller,Reducer是最明確也是最清晰的。同時Reducer也是獨(dú)立的,可以替換的。

對于UIkit層面我們無法單元測試,所以測試的主要部分是ActionReducer。這兩個模塊可以說都是pure function或者在某些條件下是pure function的,所以測試也非常的簡單。

對比

和這個模式比較像的有狀態(tài)機(jī)模式和Reactive。

狀態(tài)機(jī)模式也是實(shí)現(xiàn)對應(yīng)功能,以及對應(yīng)狀態(tài),然后通過子類化的方式去實(shí)現(xiàn)Reducer的功能。

Reactive則比較像ActionCreator,只是Reactive返回的是信號量。

使用場景

從上面可以看出這是一套非常優(yōu)秀的模塊劃分方案,但同時也會大大增加代碼量,而且需要改變以前的思維模式。而對于目前國內(nèi)的現(xiàn)狀來看,很難有這么多時間和精力讓整個項目都使用這種模式。

但是這種模式的特點(diǎn)也非常的明顯,在處理比較復(fù)雜的交互行為,并且存在較多的視圖狀態(tài)的時候,會是一種比較好的方案。比如視頻播放界面。

所以個人認(rèn)為,在一些簡單的場景下并不需要使用該方案,但是在一些復(fù)雜的交互頁面,而且又非常想要引入單元測試的場景,可以酌情考慮下這種方案。這種方案要求人們的思維方式的改變,需要有一定的函數(shù)式編程的概念。

雖然不一定會直接使用ReSwift,但是這種思想有很多值得借鑒的地方,利用這種思想做出類似的效果,以便達(dá)到可以容易進(jìn)行白盒測試的目的。

最后

以上雖然說不會全部使用該方案,但也可以部分使用。比如獨(dú)立的小模塊,亦或是app層面的一些東西。下次可以討論下app層面如何來利用單向數(shù)據(jù)流來簡化流程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容