5

I'm trying to unit test a method which has a dependency on another class. The method calls a class method on that class, essentially this:

func myMethod() {

     //do stuff
     TheirClass.someClassMethod()

}

Using dependency injection technique, I would like to be able to replace "TheirClass" with a mock, but I can't figure out how to do this. Is there some way to pass in a mock class (not instance)?

EDIT: Thanks for the responses. Perhaps I should have provided more detail. The class method I am trying to mock is in an open source library.

Below is my method. I am trying to test it, while mocking out the call to NXOAuth2Request.performMethod. This class method issues a network call to get the authenticated user's info from our backend. In the closure, I am saving this info to the global account store provided by the open source library, and posting notifications for success or failure.

func getUserProfileAndVerifyUserIsAuthenticated() {

    //this notification is fired when the refresh token has expired, and as a result, a new access token cannot be obtained
    NSNotificationCenter.defaultCenter().addObserver(self, selector: "didFailToGetAccessTokenNotification", name: NXOAuth2AccountDidFailToGetAccessTokenNotification, object: nil)

    let accounts = self.accountStore.accountsWithAccountType(UserAuthenticationScheme.sharedInstance.accountType) as Array<NXOAuth2Account>
    if accounts.count > 0 {
        let account = accounts[0]

        let userInfoURL = UserAuthenticationScheme.sharedInstance.userInfoURL

        println("getUserProfileAndVerifyUserIsAuthenticated: calling to see if user token is still valid")
        NXOAuth2Request.performMethod("GET", onResource: userInfoURL, usingParameters: nil, withAccount: account, sendProgressHandler: nil, responseHandler: { (response, responseData, error) -> Void in

            if error != nil {
                println("User Info Error: %@", error.localizedDescription);
                NSNotificationCenter.defaultCenter().postNotificationName("UserCouldNotBeAuthenticated", object: self)
            }
            else if let data = responseData {
                var errorPointer: NSError?
                let userInfo = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &errorPointer) as NSDictionary

                println("Retrieved user info")
                account.userData = userInfo

                NSNotificationCenter.defaultCenter().postNotificationName("UserAuthenticated", object: self)
            }
            else {
                println("Unknown error retrieving user info")
                NSNotificationCenter.defaultCenter().postNotificationName("UserCouldNotBeAuthenticated", object: self)
            }
        })
    }
}
Mike Taverne
  • 9,156
  • 2
  • 42
  • 58
  • 1
    Dependency injection, your class should have a constructor that accepts `TheirClass`. – Joe Jan 23 '15 at 18:13
  • How do I pass a class versus an instance of a class? – Mike Taverne Jan 23 '15 at 18:14
  • The constructor should accept an instance in which you will pass the mock object. – Joe Jan 23 '15 at 18:14
  • I can't use a mock instance because the method that I must call on the real object is a class method, not an instance method. – Mike Taverne Jan 23 '15 at 18:17
  • If the class method does not have global state or produces side effects then just focus on unit testing the static method and you won't need to test it inside of `myMethod`. http://programmers.stackexchange.com/questions/5757/is-static-universally-evil-for-unit-testing-and-if-so-why-does-resharper-recom – Joe Jan 23 '15 at 18:46

1 Answers1

7

In Swift this is better done by passing a function. There are many ways to approach this, but here is one:

func myMethod(completion: () -> Void = TheirClass.someClassMethod) {
    //do stuff
    completion()
}

Now you can pass a completion handler, while existing code will continue to use the default method. Notice how you can refer to the function itself (TheirClass.someClassMethod). You don't have to wrap it up in a closure.

You may find it better to let the caller just pass this all the time rather than making it a default. That would make this class less bound to TheirClass, but either way is fine.

It's best to integrate this kind of loose coupling, design-for-testability into the code itself rather than coming up with clever ways to mock things. In fact, you should ask yourself if myMethod() should really be calling someClassMethod() at all. Maybe these things should be split up to make them more easily tested, and then tie them together at a higher level. For instance, maybe myMethod should be returning something that you can then pass to someClassMethod(), so that there is no state you need to worry about.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Hello Rob, I came across this solution by looking for something very similar, In my case I have a slightly different problem, because the static method I should test accepts also a parameter, how could I work this around? – dev_mush Feb 10 '17 at 23:48
  • I don't understand the question. Rewrite the method to accept a closure that accepts a parameter. It's the same thing. – Rob Napier Feb 11 '17 at 01:33
  • My problem was with the default value and with obj-c interoperability, if you have some time to spend, I asked a question about that here: http://stackoverflow.com/questions/42170690/mocking-a-static-class-method-in-a-swift-unit-test-in-a-swifty-way – dev_mush Feb 11 '17 at 01:53
  • Lovely solution, but I have a question about chaining these together. Say I want to test hisMethod() that calls myMethod(), but I want to inject that myMethod() to be replaced by a custom implementation. When adding the new myMethod as a parameter to hisMethod, I must specify the input parameters, but I can't omit the parameter that has a default, namely the injected method into myMethod. Must I include in parameters of hisMethod both the custom myMethod and the myMethod injected method that will never even be called? Or can I define a function parameter without specifying inputs with defaults – Denis Balko Apr 02 '18 at 00:21