如何对 Swift 中调用的私有方法进行单元测试

How to unit test that private method is called in Swift

我有一个 ViewController class,它显示了一系列有两个选择的弹出视图。每两个选择弹出视图不同

Popup1 - Choice1 -> Choice1Popup

Popup1 - Choice2 -> Choice2Popup

我希望呈现 Popup1 的方法是 public,但我希望呈现 Choice1Popup 和 Choice2Popup 的其他方法是私有的。

如果我决定我需要测试 Choice1Popup 和 Choice2Popup,那么我可能必须将它们设为内部而不是私有,但它们不太可能在任何其他地方使用。

我想编写一个单元测试来测试在触摸 Choice1 的按钮时调用显示 Choice1Popup 的方法。我使用了一个带有方法类型变量的协议来允许模拟注入弹出演示者的模拟版本。我对自己的方法不是 100% 满意,所以我想就是否有更好的方法征求意见。

顺便说一句,我对内部和私有感到矛盾。能够测试我的私有方法会很好,但我不希望它们能够从除单元测试之外的任何地方调用并使它们在内部公开。

这是代码,底部是一个单元测试:

// protocol to be used by both UserChoices class and UserChoicesMock for method injection
protocol UserChoicesPrivateUnitTesting {
    static var choice1Method:(UIViewController) -> Void { get set }
    static var choice2Method:(UIViewController) -> Void { get set }
}

// this popup that will be presented with a public method
public class ChoiceViewController:UIViewController {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subjectLabel: UILabel!
    @IBOutlet weak var choice1Button: UIButton!
    @IBOutlet weak var choice2Button: UIButton!

     var choice1Action:(() -> Void)?
     var choice2Action:(() -> Void)?

    //    ...
}

public class UserChoices: UIViewController, UserChoicesPrivateUnitTesting {
    static var choice1Method: (UIViewController) -> Void = choice1
    static var choice2Method: (UIViewController) -> Void = choice2

    private static func choice1(onTopViewController: UIViewController) {
    //present choice1Popup
    }

    private static func choice2(onTopViewController: UIViewController) {
    //present choice2Popup
    }

    public static func presentChoiceViewController(onTopViewController: UIViewController, ChoiceViewController: ChoiceViewController = ChoiceViewController.instantiateFromAppStoryBoard(appStoryBoard: .MenuStoryboard)) {
        let isCustomAnimated = true
    //        ChoiceViewController.transitioningDelegate = transitionDelegate

        ChoiceViewController.choice1Action = { [weak onTopViewController]() in
            guard let weakSelf = onTopViewController else {
                return
            }
            weakSelf.dismiss(animated: false, completion: nil)
            UserChoices.choice1Method(onTopViewController!)
        }

        ChoiceViewController.choice2Action = { [weak onTopViewController]() in
            guard let weakSelf = onTopViewController else {
                return
            }
            weakSelf.dismiss(animated: false, completion: nil)
            UserChoices.choice2Method(onTopViewController!)
        }
        onTopViewController.present(ChoiceViewController, animated: isCustomAnimated, completion: nil)
    }
}

import XCTest
@testable import ChoiceModule

public class UserChoicesMock:UserChoicesPrivateUnitTesting {
    static public var choice1Method: (UIViewController) -> Void = choice1
    static public var choice2Method: (UIViewController) -> Void = choice2
    static var choice1MethodCalled = false
    static var choice2MethodCalled = false

    static func choice1(onTopViewController: UIViewController) {
        choice1MethodCalled = true
    }

    static func choice2(onTopViewController: UIViewController) {
        choice2MethodCalled = true
    }
}

class UserChoicesTests: XCTestCase {

    func testChoice1CallsPrivateChoice1Method() {
        // This is an example of a functional test case.
        let vc = UIViewController()
        let choiceViewController = ChoiceViewController.instantiateFromAppStoryBoard(appStoryBoard: .MenuStoryboard)

        UserChoices.choice1Method = UserChoicesMock.choice1Method

        UserChoices.presentChoiceViewController(onTopViewController: vc, ChoiceViewController: choiceViewController)

        choiceViewController.choice1Button.sendActions(for: .touchUpInside)

        if UserChoicesMock.choice1MethodCalled == false {
            XCTFail("choice1Method not called")
        }
    }
}

测试无法访问任何已声明的内容 private。只要测试代码 @testable import.

,他们就可以访问任何声明为 internal 的内容

当您感到恶心时,"But I shouldn't have to expose this," 认为您的 class 实际上有多个接口。有 "everything it does interface" 和 "parts needed by production code interface." 关于这个有很多事情需要考虑:

  • 是否有另一种试图逃脱?
  • 是否有另一种协议来表达接口的子集?这可以被其余的生产代码使用。
  • 或者它可能就像一个家庭影院放大器,其中 "controls you don't need that often" 隐藏在面板后面。别着急。