但是以上的內容我都是帶過,不會花太時間解釋
我們的注意力會放在單元測試上。
這個是目前View的畫面,目前還沒有套用任何邏輯。
目標是讓輸入匡輸入5個字元,就算是符合規範。
帳號與密碼都符合規範,Login in 的按鈕才可以按。
這是目前的view Controller,什麼東西都沒有。
class LoginPageViewController:UIViewController{}
先在 LoadView 讀取客製化的view 。
class LoginPageViewController:UIViewController{
var loginPageView:LoginPageView!
//MARK: - LoadView()
override func loadView() {
loginPageView = LoginPageView()
self.view = loginPageView
}
}
很好,已經讀到畫面了。
但是還是沒辦法有邏輯上的連動
我把binding的過程分開,當然你可以寫在一起。
為了refacter方便我會分開寫。
//MARK: - DataBinding()
func dataBinding(){
//observable
//vaild
//bind
}
}
這裡我創建了兩個推送事件序列,這兩個推送的物件是 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
}
這裡有三道業務邏輯。
//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)
}
以上我們已經完成了資料綁定,已經可以做互動了。
我們從一個測試類別開始
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的物件。
我們是在 LoadView() 實例化物件的。所以我們讓 sut 執行 LoadView()
override func setUp() {
super.setUp()
sut = LoginPageViewController()
sut.loadView()
}
command + u 再測試一次。
測試成功了
我們從一個空白的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 UIKit
import RxCocoa
import RxSwift
let minimalUsernameLength = 5
let minimalPasswordLength = 5
class 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已經算是完成了,我們來對他做測試吧。
配置好業務邏輯需要的基本設定
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
測試成功
MVC在單元測試時,必須要實例化view(MVC變著不納入討論),這使單元測試偏離了原生單元測試的設計。因為單元測試就應該測試業務邏輯,他不關心UI上面的變化。
MVVM 與 MVC 的差異就是把業務邏輯抽離出來,這讓單元測試上有很大的幫助,我可以更專注在業務邏輯上的測試,而不用擔心View的生命週期。