从数据流动角度解决测试难题

上篇讲述了增强逻辑功能测试而改进MVC为MVP,但是这样做可能还不够彻底,现在来讨论另一个纯粹从测试角度设计的框架。

首先我们来明确一下,测试中最核心的东西是什么。当然是数据,我们永远是围绕着数据来的,那么之前一些架构的问题是什么。无论哪个框架,数据的流通都是双向的,当数据流通成为单向了会怎么样呢?

in data ==> Module ==> out data

这样我们伪造数据进行测试就会非常方便了。按照这个思想就有了数据单向流通的架构。

数据单向流通的实现

这个概念最早是在web中提出的,应用在React里,官方的方案是Redux。现在swift也提出了一种实现ReSwift

我在之前写React的时候使用过这种方案,从开发角度来说,这种方案会大大增加开发难度,代码量也会大量增加,而且开发思路也需要从以前的思考方式转换过来。但是如果我们把这个思路转换过来,其实对整个流程是更加简化和分离的。

从测试角度看,我觉得无疑是我知道的最可测的一种框架,甚至可以测试部分视图的逻辑。

那么总的来说,很难说这种结构的好坏,就算不考虑增加的开发时间,也是一种难以给以一种评价的方案。

(Redux/ReSwift)框架介绍

方案的几个核心是:

  • 数据的单向流通
  • 每个视图都可以看做一个状态机
  • pure function

关于pure function,我就不做太多介绍了,简单的说,就是同一输入必定会有相同的输出,是非常容易测试的一种函数。

首先,我们来看一下官方的架构图。

可以看到,数据流动方向都是朝一个方向进行的。那么下面从每个模块来介绍下,还是以star button为例子。

State

视图状态机,也是所有会更新界面数据保存的地方,可以认为相当于ViewModel。

首先我们star会有以下几种视觉样式

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

所以State可以定义为

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

Action

首先我们定义几种状态机转换的Action类型

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

以及相应的功能以及状态变更,这里异步请求采用延迟来代表。

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

视图层其实很简单,只需要根据State的不同来更新就可以了。注意的是,更新都是无状态的,和上一个状态无关,所以view层是个无状态层。

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

状态转换器,唯一可以更新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
}

数据传递

那么最重要的就是数据如何传递的了。首先要明确的是每个模块能够修改的,或者说是传递的,只能是下个模块。

比如,用户star button触发了一个事件:

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))
    }
}

此时会创建Action,也就是将view事件转换为Action。然后会传递到store中,store会调用Reducer进行处理。Reducer更新state之后又会触发store的subscribe事件,回到view的func newState(state: StarState)

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

大概的一个流程就是这样了。

接下来说说这样做的模块化的优势。

模块化和测试性

首先,我们需要有函数式编程的概念,函数也是一等公民,所以ActionCreatorReducer都是独立的模块。

作为使用者,我们在不需要像MVC一样知道这些api所代表的操作功能,相对应的,我们需要去了解一个模块的动作(Action),比如以上例子就是

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

这样的划分比MVC要友好的多,真正的把逻辑功能从原本的C中分离开。需要触发这个行为也非常简单store.dispatch(star(id: id))。相比MVP,行为更加的独立,每个行为之间完全没有联系,也不会产生干扰影响。同时因为每个行为的独立性,可复用程度也就越高。

Reducer则代表了view层的更新,也可以非常明确的知道每个状态的变更发生了什么。相比其他模式,将界面更新完全交给view或者Controller,Reducer是最明确也是最清晰的。同时Reducer也是独立的,可以替换的。

对于UIkit层面我们无法单元测试,所以测试的主要部分是ActionReducer。这两个模块可以说都是pure function或者在某些条件下是pure function的,所以测试也非常的简单。

对比

和这个模式比较像的有状态机模式和Reactive。

状态机模式也是实现对应功能,以及对应状态,然后通过子类化的方式去实现Reducer的功能。

Reactive则比较像ActionCreator,只是Reactive返回的是信号量。

使用场景

从上面可以看出这是一套非常优秀的模块划分方案,但同时也会大大增加代码量,而且需要改变以前的思维模式。而对于目前国内的现状来看,很难有这么多时间和精力让整个项目都使用这种模式。

但是这种模式的特点也非常的明显,在处理比较复杂的交互行为,并且存在较多的视图状态的时候,会是一种比较好的方案。比如视频播放界面。

所以个人认为,在一些简单的场景下并不需要使用该方案,但是在一些复杂的交互页面,而且又非常想要引入单元测试的场景,可以酌情考虑下这种方案。这种方案要求人们的思维方式的改变,需要有一定的函数式编程的概念。

虽然不一定会直接使用ReSwift,但是这种思想有很多值得借鉴的地方,利用这种思想做出类似的效果,以便达到可以容易进行白盒测试的目的。

最后

以上虽然说不会全部使用该方案,但也可以部分使用。比如独立的小模块,亦或是app层面的一些东西。下次可以讨论下app层面如何来利用单向数据流来简化流程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容