从单元测试探讨 MVC to MVVM 的差异

从单元测试探讨 MVC to MVVM 的差异

你在这里学到什么?

用 RxSwift DataBinding从 MVC 业务逻辑抽离比较 MVC 与 MVVM Unit test 的差异

但是以上的内容我都是带过,不会花太时间解释
我们的注意力会放在单元测试上。

如何开始

目标

这个是目前View的画面,目前还没有套用任何逻辑。
目标是让输入匡输入5个字元,就算是符合规範。
帐号与密码都符合规範,Login in 的按钮才可以按。

Get start

读取客製化的View

这是目前的view Controller,什么东西都没有。

class LoginPageViewController:UIViewController{}

先在 LoadView 读取客製化的view 。

class LoginPageViewController:UIViewController{    var loginPageView:LoginPageView!    //MARK: - LoadView()    override func loadView() {        loginPageView = LoginPageView()        self.view = loginPageView    }}

很好,已经读到画面了。
但是还是没办法有逻辑上的连动

DataBinding

我把binding的过程分开,当然你可以写在一起。
为了refacter方便我会分开写。

//MARK: - DataBinding()    func dataBinding(){        //observable        //vaild        //bind    }}

创建Obserable

这里我创建了两个推送事件序列,这两个推送的物件是 UITextField的text属性。

//MARK: - DataBinding()    func dataBinding(){        //observable        let usernameUITextFieldObservable = loginPageView.usernameTextField.rx.text.orEmpty        let passnameUITextFieldObservable = loginPageView.passwordTestField.rx.text.orEmpty        //vaild        let usernameVaild = usernameUITextFieldObservable            .map{ $0.count >= minimalUsernameLength}        let passwordVaild = passnameUITextFieldObservable            .map{ $0.count >= minimalPasswordLength}        let everythingVaild = Observable.combineLatest(usernameVaild,passwordVaild)            .map{ $0 && $1 }        //bind    }

创建业务逻辑

这里有三道业务逻辑。

依照usernameUITextFieldObservable的字数传递Boolean依照passnameUITextFieldObservable的字数传递Boolean依照usernameVaild与passwordVaild传递的Boolean 依照 AND运算子 传递 Boolean
//MARK: - DataBinding()    func dataBinding(){        //observable        let usernameUITextFieldObservable = loginPageView.usernameTextField.rx.text.orEmpty        let passnameUITextFieldObservable = loginPageView.passwordTestField.rx.text.orEmpty        //vaild        let usernameVaild = usernameUITextFieldObservable            .map{ $0.count >= minimalUsernameLength}        let passwordVaild = passnameUITextFieldObservable            .map{ $0.count >= minimalPasswordLength}        let everythingVaild = Observable.combineLatest(usernameVaild,passwordVaild)            .map{ $0 && $1 }        //bind        usernameVaild.bind(to: loginPageView.usernameValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        passwordVaild.bind(to: loginPageView.passwordValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        everythingVaild.bind(to: loginPageView.loginButton.rx.isEnabled)            .disposed(by: disposeBag)    }

绑定

对应要做出反应的参数。

这边要注意 dispose 的回收机制。
有兴趣可以参考autoreleasepool,这是相同的回收机制。

//MARK: - DataBinding()    func dataBinding(){        //observable        let usernameUITextFieldObservable = loginPageView.usernameTextField.rx.text.orEmpty        let passnameUITextFieldObservable = loginPageView.passwordTestField.rx.text.orEmpty        //vaild        let usernameVaild = usernameUITextFieldObservable            .map{ $0.count <= minimalUsernameLength}        let passwordVaild = passnameUITextFieldObservable            .map{ $0.count <= minimalPasswordLength}        let everythingVaild = Observable.combineLatest(usernameVaild,passwordVaild)            .map{ $0 && $1 }        //bind        usernameVaild.bind(to: loginPageView.usernameValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        usernameVaild.bind(to: loginPageView.passwordValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        everythingVaild.bind(to: loginPageView.loginButton.rx.isEnabled)            .disposed(by: disposeBag)    }


以上我们已经完成了资料绑定,已经可以做互动了。

MVC 业务逻辑测试

我们从一个测试类别开始

class MVCLearnTests: XCTestCase {}

我们配置好 sut <-- 受测试的物件

class MVCLearnTests: XCTestCase {    var sut : LoginPageViewController!    override func setUp() {        super.setUp()        sut = LoginPageViewController()    }    override func tearDown() {        super.tearDown()        sut = nil    }}

建议善用 setUp 与 tearUp 的回收机制。避免因为没有清除影响其他测试。
延伸阅读: zombie objects 。

基本的配置完成后,可以开始写测试的函式了。

    func testLoginPageViewController_whenUsernameIsVaild_usernameVaildUIlabelisEnable(){        //given        //when        //then    }

先写好三个测试流程的步骤:
这是为了方便建制这个 Test的流程。

Given 在特定的条件下
When 当某个行为发生时
Then 预期要发生的结果

延伸阅读:命名规範

    func testLoginPageViewController_whenUsernameIsVaild_usernameVaildUIlabelisEnable(){        //given        let text = "12345"        //when        sut.loginPageView.usernameTextField.text = text        //then        let isEnabled = sut.loginPageView.usernameTextField.isEnabled        XCTAssertEqual(isEnabled, true)    }

测试逻辑写完后 command + u 测试看看。

结果发生问题了,这是为什么呢?

因为我们要测试的物件牵涉到UI

因此我们要实例化UI的物件。

我们是在 LoadView() 实例化物件的。所以我们让 sut 执行 LoadView()

    override func setUp() {        super.setUp()        sut = LoginPageViewController()        sut.loadView()    }

command + u 再测试一次。

测试成功了

在这里我们注意到两件事:

Unit test 本身是不牵涉到 view 的生命週期我们为了测试业务逻辑,却把view拖到这个浑水了(实例化了view)

MVC to MVVM

我们从一个空白的class开始。

class LoginPageViewModel{}

然后把刚刚 vaild 的片段(业务逻辑)贴过来。
然后稍作改写一下 viewModel 就成形了

class LoginPageViewModel{    var usernameVaild:Observable<Bool>    var passwordVaild:Observable<Bool>    var everythingVaild:Observable<Bool>    init (username:Observable<String>,password:Observable<String>){        //vaild        usernameVaild = username            .map{ $0.count >= minimalUsernameLength}        passwordVaild = password            .map{ $0.count >= minimalPasswordLength}        everythingVaild = Observable.combineLatest(usernameVaild,passwordVaild)            .map{ $0 && $1 }    }}

接下来把业务逻辑抽离。

import UIKitimport RxCocoaimport RxSwiftlet minimalUsernameLength = 5let minimalPasswordLength = 5class LoginPageViewController:UIViewController{    var loginPageView:LoginPageView!    var disposeBag:DisposeBag!    var viewModel : LoginPageViewModel!        //MARK: - LoadView()    override func loadView() {        loginPageView = LoginPageView()        self.view = loginPageView            }//MARK: - ViewDidLoad()    override func viewDidLoad() {        super.viewDidLoad()        disposeBag = DisposeBag()        dataBinding()    }//MARK: - DataBinding()    func dataBinding(){        //observable        viewModel = LoginPageViewModel(            username: loginPageView.usernameTextField.rx.text.orEmpty.asObservable(),            password: loginPageView.passwordTestField.rx.text.orEmpty.asObservable())                //bind        viewModel.usernameVaild.bind(to: loginPageView.usernameValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        viewModel.passwordVaild.bind(to: loginPageView.passwordValidUILabel.rx.isHidden)            .disposed(by: disposeBag)        viewModel.everythingVaild.bind(to: loginPageView.loginButton.rx.isEnabled)            .disposed(by: disposeBag)    }}

执行一下专案,可以正常运作。
这样MVVM已经算是完成了,我们来对他做测试吧。

MVVM 业务逻辑测试

配置好业务逻辑需要的基本设定

class LoginPageViewModelTests: XCTestCase {    var sut : LoginPageViewModel!    var usernameObservable:Observable<String>!    var passwordObservable:Observable<String>!    var disposeBag:DisposeBag!    override func setUp() {        super.setUp()        disposeBag = DisposeBag()    }    override func tearDown() {        super.tearDown()        sut = nil        usernameObservable = nil        passwordObservable = nil        disposeBag = nil    }}

配置完成后可以开始写测试函式了

    func testLoginPageViewModel_usernameIsValid_true(){        //given        //when        //then    }

测试流程的注解。

    func testLoginPageViewModel_usernameIsValid_true(){        //given        usernameObservable = Observable.create({ (observer) -> Disposable in            observer.onNext("12345")            observer.onCompleted()            return Disposables.create()        })        passwordObservable = Observable.create({ (observer) -> Disposable in            observer.onCompleted()            return Disposables.create()        })        //when        sut = LoginPageViewModel(            username: usernameObservable,            password: passwordObservable)        //then        sut.usernameVaild.bind { (bool) in            XCTAssertEqual(bool, true)        }.disposed(by: disposeBag)    }

command + u
测试成功

总结

1. MVC的单元测试必须实例化View。

MVC在单元测试时,必须要实例化view(MVC变着不纳入讨论),这使单元测试偏离了原生单元测试的设计。因为单元测试就应该测试业务逻辑,他不关心UI上面的变化。

2. MVVM 只是 MVC refactor 的过程。

MVVM 与 MVC 的差异就是把业务逻辑抽离出来,这让单元测试上有很大的帮助,我可以更专注在业务逻辑上的测试,而不用担心View的生命週期。

讨论

将业务逻辑拆开有很多方法,而MVVM仅仅是其中一种。DataBinding有哪些方法?

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章