1

I have a ViewController class that presents a series of two choice popup views. Each two choice popup view is different.

Popup1 - Choice1 -> Choice1Popup

Popup1 - Choice2 -> Choice2Popup

I intend the method to present Popup1 to be public, but I want the other methods that present Choice1Popup and Choice2Popup to be private.

If I decide I need to test Choice1Popup and Choice2Popup then I may have to make them internal instead of private, but they are unlikely to ever be used from any other place.

I want to write a unit test that tests when the button for Choice1 is touched that the method that presents Choice1Popup is called. I've used a protocol with method type variables to allow a Mock to inject the Mock versions of the popup presenters. I'm not feeling 100% comfortable about my approach so I wanted to get input as to whether or not there is a better way.

An aside I'm feeling conflicted about internal versus private. It would be nice to be able to test my private methods but I don't want them to be able to be called from anywhere but a unit test and making them internal exposes them.

Here is the code and a single Unit test is at the bottom:

// 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")
        }
    }
}
bhartsb
  • 1,316
  • 14
  • 39

1 Answers1

1

Tests can't access anything declared private. They can access anything declared internal as long as the test code does @testable import.

When you get that queasy feeling, "But I shouldn't have to expose this," consider that your class actually has multiple interfaces. There's the "everything it does interface" and there's the "parts needed by production code interface." There are various things to consider about this:

  • Is there another type that is trying to get out?
  • Is there another protocol to express a subset of the interface? This could be used by the rest of production code.
  • Or maybe it's like a home theater amplifier where "controls you don't need that often" are hidden behind a panel. Don't sweat it.
Jon Reid
  • 20,545
  • 2
  • 64
  • 95
  • Yeah I understand that I can make methods internal instead of private and have access to them in my unit tests. In this case there is a sequence of two choice popups that happen. Right now only the first is public and the rest private. However, it would be nice to test them as well if I didn't have to make them internal. I would like to do an end to end test too but will have to do that with a UI test unless I make them internal. So as per the technique of testing that the public one with the mock class. That all okay to you? No better approaches? It seems the best to me. – bhartsb Jan 10 '19 at 21:19
  • What invokes your private methods? Is there any chance of having tests call through that, instead of the private methods? – Jon Reid Jan 11 '19 at 01:30
  • Choice1 and Choice2 method type variables are assigned private methods in UserChoices class. These private methods are only called from public presentChoiceViewController(). Their mock versions are injected in the unit tests. These UserChoice class private methods in turn open further 2 choice popups which use a private structure that is used to hold some gathered information. But there is no reason beyond Unit testing to expose them by making them internal. – bhartsb Jan 11 '19 at 23:43
  • I bet there's another way to introduce a testing seam. What's preventing you from using more straightforward code without `UserChoicesPrivateUnitTesting`? That is, if you wrote code without injecting the methods but just called what you wanted, what would you find difficult to intercept? Is it the `present` call? – Jon Reid Jan 12 '19 at 05:22
  • The issue is that I want to keep choice1 and choice2 methods private not internal, and since the real versions of choice1 and choice2 (as opposed to the mock versions) create more two choice popups, I don't want to call them in a unit test. I.e. I just want to test that for the first two choice popup that per the two button actions the correct method with the correct signature is called. I hope my explanation is clear enough. – bhartsb Jan 12 '19 at 06:21
  • If you stick with the code you have, I don't see how to test them while keeping them private. But it sounds like your UI is fairly complex. Have you considered using a state machine? – Jon Reid Jan 12 '19 at 06:31
  • To clarify at the first "popup level" a popup A with 2 choices is presented. When either choice is made a NSDefaults value is set and a new deeper layer popup B or popup C is next presented. Since only presenting popup A is public I'm not too interested in making the presenters for popup B and C internal just so they can be tested. Within B and C choices there is no NSDefaults value being set and used elsewhere. However C does read information from elsewhere and present use it to populate an email so I'm debating if it should be internal and tested or maybe just the struct for the info. – bhartsb Jan 12 '19 at 06:33
  • I'm also using UI tests for the end to end user taken paths through the popups. I think you also mostly answered my question with "If you stick with the code you have, I don't see how to test them while keeping them private." – bhartsb Jan 12 '19 at 06:41
  • Thank you for the explanation, it's helpful. To unit test each part, make each part internal. While there are many choices for design (from your UserChoices to a state machine, and more besides), "must be `internal` or `public`" is just a basic truth of unit testing in Swift. – Jon Reid Jan 12 '19 at 06:46
  • Thanks. I just wanted to kind of make sure I’ve been watching some of the WWDC unit test related videos and they get show pretty involved approaches so wasn’t sure if I was missing anything. Also sometimes I wonder if it’s a great trade off or not given that Swift is so concise and one can write really nice readable code but it is not testable. – bhartsb Jan 12 '19 at 22:51