使用RxSwift,TDD和MVVM以编程方式实现UIButton

时间:2019-02-26 13:46:33

标签: ios swift mvvm tdd rx-swift

我正在尝试使用RxSwift以编程方式实现UIButton,我进行了很多搜索,没有发现任何有用的东西,因为我是这个主题的新手,所以我脑海中有些疑问

我有一个带有两个按钮的视图,注册和登录,通过点击此按钮,唯一的动作是推送到新的ViewController,这是LoginViewController的代码(我知道除非有失败的测试,否则我不应该实现任何代码通过,它只是用于显示我到底想做什么):

import UIKit
import RxCocoa
import RxSwift

class LoginViewController: UIViewController {

    let disposeBag = DisposeBag()
    var loginButton: UIButton?
    var registerButton: UIButton?

    override func viewDidLoad() {
        super.viewDidLoad()
        loginButton = UIButton()
        registerButton = UIButton()

        view.addSubview(loginButton!)

        loginButton?.rx.tap
          .subscribe(onNext:{ [weak self] in
                let enterPhoneNumberVC = EnterPhoneNumberViewController(nibName: "EnterPhoneNumberViewController", bundle: nil) as EnterPhoneNumberViewController
                self?.navigationController?.pushViewController(enterPhoneNumberVC, animated: true)
            })
          .disposed(by: disposeBag)
    }
}

问题1:据我了解,由于按钮操作中没有逻辑,因此无需通知viewModel,对吗?还是应该通过视图模型来处理它?<​​/ p>

问题2:由于这是TDD项目,我应该如何测试以下提到的功能:

检查点击是否在ViewController中处理

测试触摸按钮是否确实满足我的要求

谢谢。

2 个答案:

答案 0 :(得分:1)

答案1:我要说的是,如果没有逻辑,则没有理由进行测试,也不需要通过视图模型运行代码。也就是说,在您发布的示例中,存在 逻辑。闭包中的每个?代表一个if检查和if检查是逻辑。如果显示LoginViewController而不是将其推送到导航控制器上怎么办?对该问题的常见回答是:“嗯,我知道它没有出现。我知道导航控制器存在。”正是我知道这是问题所在。

我在这里的首选方法是将按钮的点击路由到可以推到导航控制器上的东西,而无需检查它是否存在。这样一来,它的存在就毫无疑问(双关语意)。

答案2:您提到的两个功能都是副作用,需要集成测试,而不是单元测试。您可以使用UI测试来测试这种功能。

答案 1 :(得分:0)

问题1 。我同意,loginButton点击处理中的所有逻辑都与导航有关,并且非常适合视图控制器领域。

问题2 。您发现需要测试两种不同的行为:

  • LoginViewController 处理点击loginButton上的吗?
  • EnterPhoneNumberViewController是否已推送到导航堆栈?

类似于@Daniel T. answer,我建议将处理点击的代码与推送新视图控制器的代码分离

您可以通过多种方式执行此操作,我最喜欢的是在视图控制器中有一个导航委托并测试它的调用。除此之外,您还可以测试是否符合委托的类型进行了推送。

protocol LoginViewControllerNavigationDelegate: class {
  func showEnterPhoneNumber()
}
func testLoginTapCallsShowEnterPhoneNumber() {
  let navigationDelegateSpy = LoginViewControllerNavigationDelegateSpy()
  let viewController = ...
  viewController.navigationDelegate = navigationDelegateSpy
  viewController.beginAppearanceTransition(true, animated: false) // loads the view

  viewController.loginButton.sendActions(for: .touchUpInside)

  XCTAssertTrue(navigationDelegateSpy.showEnterPhoneNumberCalled)
}
class LoginViewControllerNavigationDelegateSpy: LoginViewControllerNavigationDelegate {

  private(set) var showEnterPhoneNumberCalled = false

  func showEnterPhoneNumber() {
    showEnterPhoneNumberCalled = true
  }
}

在视图控制器中,您将EnterPhoneNumberViewController的直接推送替换为

loginButton?.rx.tap
  .subscribe(onNext:{ [weak self] in
    self?.navigationDelegate?.showEnterPhoneNumber()
  })
  .disposed(by: disposeBag)

哪个组件符合LoginViewControllerNavigationDelegate由您决定。理想情况下,它将首先显示LoginViewController

为了这个例子,我们假设它是一个专用对象,我们将其称为LoginNavigator

func testShowEnterPhoenNumber() {
  let navigationController = UINavigationController(rootViewController: UIViewController())
  let navigator = LoginNavigator(navigationController)

  navigator.showEnterPhoneNumber()

  XCTAssertTrue(navigationController.topViewController is EnterPhoneNumberViewController)
}

此方法确实需要做更多的工作,但是它具有解耦的优点。如果您需要更改EnterPhoneNumberViewController的显示方式(例如从推送到模式),则可以在集中且专用的位置进行操作,而不必深入研究LoginViewController