向下转换/子类 UIViewController 用于单元测试中的模拟

Posted

技术标签:

【中文标题】向下转换/子类 UIViewController 用于单元测试中的模拟【英文标题】:Downcast / subclass UIViewController for mock in unit test 【发布时间】:2019-04-08 21:32:05 【问题描述】:

我有一个单元测试,我想创建一个UIViewController 的子类版本,例如Test1ViewController。具体来说,我想重写这个类的present 方法。

我有一个视图控制器扩展,它根据其类名实例化视图控制器。

public class func instanceFromStoryboard<T>(storyboard: Storyboard) -> T 
    return UIStoryboard(name: storyboard.rawValue, bundle: nil).instantiateViewController(withIdentifier: String(describing: T.self)) as! T

还有一个故事板类。

public enum Storyboard: String 
    case main = "Main"

在我的单元测试中,我从Test1ViewController 创建了一个子类。

class Test2ViewController: Test1ViewController 
    var presented: Bool = false
    override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) 
        presented = true
    

如何使用我的扩展方法从情节提要中检索视图控制器,然后向下转换/子类化为Test2ViewController

【问题讨论】:

【参考方案1】:

因为情节提要中的对象实际上是编码对象,所以它们不能被解码并转换为其他任何东西。这是使用故事板的一个缺点。你在故事板中的内容就是你得到的东西。

如果可以,请使用基于 XIB 的视图控制器而不是基于情节提要的视图控制器。使用 XIB(以及编程视图控制器),测试可以实例化子类。

如果没有,那么您需要为视图控制器引入一个后门。这将是不幸的,因为这意味着将测试代码混合到您的生产代码中。

【讨论】:

这很好地解释了为什么它不能正常工作。【参考方案2】:

@Jon Reid 的answer 很好地总结了使用故事板的局限性。

如果您的最终目标是验证被测 UIViewController 是否呈现了某些内容,您是否考虑过检查 presentedViewController 属性?

// Create an asynchronous expectation to verify the view controller has presented
// something.
_ = expectation(
  for: NSPredicate(
    block:  input, _ -> Bool in
      guard let viewController = input as? UIViewController else  return false 
        // If you care about the type of the presented view controller you could
        // use `is` here to verify it
        return viewController.presentedViewController != nil
      
    ),
    evaluatedWith: viewControllerUnderTest,
    handler: .none
)

viewControllerUnderTest.doSomething()

waitForExpectations(timeout: 1, handler: nil)

另一种方法,它需要更多的工作,但随着时间的推移,通过使您的视图控制器更容易在导航流程中移动而获得回报,是对所有演示文稿使用委托。

在测试中,您不会检查是否已呈现某些内容,但前提是已使用Spy test double 调用了呈现某些内容的导航委托方法。 如果您对这种方法感到好奇,很乐意提供更多详细信息。

【讨论】:

【参考方案3】:

@mokagios answer 应该可以工作。我更喜欢另一种方法。

func test_presentationOfViewController() 
  // Arrange
  let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
  window.rootViewController = sut
  window.makeKeyAndVisible()

  // Act
  sut.presentNext()

  // Assert
  XCTAssertTrue(sut.presentedViewController is DetailViewController)

【讨论】:

以上是关于向下转换/子类 UIViewController 用于单元测试中的模拟的主要内容,如果未能解决你的问题,请参考以下文章

使用 django-model-utils 自动向下转换为子类

自动将父类中的 shared_ptr 向下转换为子类类型

是否可以从父类指针向下转换为第二个子类中的子类指针?

向上转型和向下转型

从向上向下转型到----抽象类接口

引用类型的转换