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

Posted

技术标签:

【中文标题】如何对在 Swift 中调用的私有方法进行单元测试【英文标题】:How to unit test that private method is called in Swift 【发布时间】:2019-01-08 03:13:53 【问题描述】:

我有一个 ViewController 类,它呈现一系列的两个选择弹出视图。每两个选项的弹出视图都是不同的。

Popup1 - Choice1 -> Choice1Popup

Popup1 - Choice2 -> Choice2Popup

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

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

我想编写一个单元测试来测试当 Choice1 的按钮被触摸时是否调用了呈现 Choice1Popup 的方法。我使用了带有方法类型变量的协议,以允许 Mock 注入弹出演示者的 Mock 版本。我对自己的方法不是 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")
        
    

【问题讨论】:

***.com/a/61348578/6799777 【参考方案1】:

测试无法访问声明为 private 的任何内容。他们可以访问声明为internal 的任何内容,只要测试代码执行@testable import

当你感到“但我不应该公开这个”时,你会觉得你的类实际上有多个接口。有“它所做的一切接口”和“生产代码接口所需的部分”。有很多事情需要考虑:

是否还有其他类型的人正在尝试脱身? 是否有其他协议来表达接口的子集?其余的生产代码都可以使用它。 或者,它可能就像一个家庭影院放大器,其中“您不需要经常使用的控件”隐藏在面板后面。不要出汗。

【讨论】:

是的,我知道我可以将方法设为内部而不是私有,并且可以在我的单元测试中访问它们。在这种情况下,会发生一系列两个选择弹出窗口。现在只有第一个是公开的,其余的是私有的。但是,如果我不必将它们设为内部,那么对它们进行测试也会很好。我也想做一个端到端的测试,但除非我把它们放在内部,否则必须用 UI 测试来做。因此,根据模拟类的公共测试技术。这一切对你好吗?没有更好的方法?这对我来说似乎是最好的。 什么调用了你的私有方法?有没有机会通过它而不是私有方法调用测试? Choice1 和 Choice2 方法类型变量在 UserChoices 类中分配了私有方法。这些私有方法只能从公共 presentChoiceViewController() 调用。他们的模拟版本被注入到单元测试中。这些 UserChoice 类私有方法依次打开另外 2 个选择弹出窗口,这些弹出窗口使用私有结构,用于保存一些收集的信息。但是除了单元测试之外,没有理由通过将它们设置为内部来公开它们。 我敢打赌还有另一种引入测试接缝的方法。是什么阻止您在没有UserChoicesPrivateUnitTesting 的情况下使用更直接的代码?也就是说,如果您编写代码而不注入方法而只是调用您想要的,您会发现难以拦截的内容是什么?是present 电话吗? 问题是我想让choice1 和choice2 方法私有而不是内部的,而且由于choice1 和choice2 的真实版本(与模拟版本相反)会创建更多的两个选择弹出窗口,所以我不这样做不想在单元测试中调用它们。 IE。我只想测试对于前两个选择弹出窗口,每个按钮操作都会调用具有正确签名的正确方法。我希望我的解释足够清楚。

以上是关于如何对在 Swift 中调用的私有方法进行单元测试的主要内容,如果未能解决你的问题,请参考以下文章

如何通过phpunit对一个方法进行单元测试,该方法具有多个内部调用保护/私有方法?

如何在 Swift 中对私有或内部函数进行单元测试?

如何在单元测试之前等待组件的挂载回调中的异步调用完成?

如何在 Typescript 中对私有方法进行单元测试

如何在Typescript中对私有方法进行单元测试

如何给一个私有方法做单元测试