4

I've got a Swift class that I'm unit testing. The main purpose of the class is to make HTTP calls. I just finished mocking all networking requests using Mockingjay, but I want to make sure that going forward I don't forget to mock future requests. While I was doing the initial mocking I replaced the base URL I'm using with one that doesn't work, but I'd like to keep that in place only for my tests. The class under test looks like this:

public class MyWebServiceWrapper {
    ...

    public class var baseURL: String {
        return "https://api.thesite.com"
    }

    ...
}

I've tried to use OCMock from an Objective-C class to replace baseURL's implementation, and I've tried using method swizzling as described on NSHipster as well (I made the class derive from NSObject and replaced baseURL with a method instead of the property shown above - I'd rather not swizzle, especially since I otherwise don't need the class to be an NSObject subclass).

This is the code I used to perform the attempted swizzle, which had no effect. For both the attempted solutions below, I rewrote my class under test like so (since it looked like neither approach would work well with Swift properties or pure Swift classes):

@objc public class MyWebServiceWrapper: NSObject {
    ...

    public class var baseURL: String {
        return baseURLValue()
    }

    public class func baseURLValue() -> String {
        return "https://api.thesite.com"
    }

    ...
}

This was my swizzling attempt (in Swift). Based on the log, I hit the else statement every time, in case it helps figure out what's going on.

extension MyWebServiceWrapper {

    public override class func initialize() {
        struct Static {
            static var token: dispatch_once_t = 0
        }

        if self !== MyWebServiceWrapper.self {
            return
        }

        dispatch_once(&Static.token) {
            NSLog("Replacing MyWebServiceWrapper base URL...")
            let originalSelector = #selector(baseURLValue)
            let swizzledSelector = #selector(networkFreeBaseURL)

            guard let klass = object_getClass(self) else {
                return
            }

            let originalMethod = class_getClassMethod(klass, originalSelector)
            let swizzledMethod = class_getClassMethod(klass, swizzledSelector)

            let didAddMethod = class_addMethod(klass, originalSelector,
                                               method_getImplementation(swizzledMethod),
                                               method_getTypeEncoding(swizzledMethod))

            if didAddMethod {
                NSLog("MyWebServiceWrapper method added")
                class_replaceMethod(klass, swizzledSelector, method_getImplementation(originalMethod),
                                    method_getTypeEncoding(originalMethod))
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod)
                NSLog("MyWebServiceWrapper implementation replaced")
            }
        }
    }

    class func networkFreeBaseURL() -> String {
        return "https://tests-shouldnt-hit-the-network.com"
    }

}

This is how I tried to replace the method using OCMock (in Objective-C). The code is called during the configuration of my unit tests.

id wrapperMock = OCMClassMock([MyWebServiceWrapper class]);
OCMStub(ClassMethod([wrapperMock baseURLValue])).andReturn(@"https://tests-shouldnt-hit-the-network.com");

What's the correct way to achieve my goal of returning an invalid value from baseURL?

Dov
  • 15,530
  • 13
  • 76
  • 177

1 Answers1

1

I'm not totally happy with it (in other words, I would still love alternative answers), since it required a modification to the code of the class under test, but this is what I've ended up with. It gets the job done, and can't easily be abused by non-testing code, since I made the override variable internal.

public class MyWebServiceWrapper {
    ...

    /// Only use for testing!
    internal static var baseURLOverride: String?

    public class var baseURL: String {
        return baseURLOverride ?? "https://api.thesite.com"
    }

    ...
}

And from inside my test configuration:

@testable
import MyFramework

...

// Inside test configuration
MyWebServiceWrapper.baseURLOverride = "https://tests-shouldnt-hit-the-network.com"
Dov
  • 15,530
  • 13
  • 76
  • 177