kylef / Mockingjay

An elegant library for stubbing HTTP requests with ease in Swift

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Matchers running slowly

abbeycode opened this issue · comments

I've got a test that's a sort of integration/stress test for my networking code. When I switched from having my networking code check for cached files my tests placed in a temp directory to using Mockingjay, the tests slowed down a lot. I profiled with Instruments, and found that most of the test's time is being spent in MockingjayProtocol.stubForRequest(NSURLRequest) -> Stub?, and specifically in my custom matcher, which looks like this:

public func urlPathEquals(path: String) -> (request:NSURLRequest) -> Bool {
    return { request in
        request.URL?.path == path
    }
}

Before this test case runs, I loop through my large number of JSON files and create a matcher like this for each one, so there is a large number of mocks configured. What can I do to speed this up? My test is taking so long that it's causing a failure on my build server.

Hi @abbeycode,

Can you please elaborate on roughly how many mocks are configured?

@kylef I've got 7227… I know that's a lot…

Okay, so looping over 7227 different matchers multiple times is probably a little too much. Since they're all paths, I wonder if we can perhaps override stubForRequest to use a dictionary of URI paths. Then the lockup should be considerably faster.

Something along the lines of:

var stubs: [String: Builder] = [:]

class CustomProtocol : MockingjayProtocol {
  class func addStub(path: String, builder: Builder) {
    stubs[path] = builder
  }

  class func stubForRequest(request: NSURLRequest) -> Stub? {
    if let path = request.URL?.path, builder = stubs[path] {
      return /* some stub for the builder */
    }

    return nil
  }
}

This would certainly need some changes to Mockingjay to provide a more customisable API, but I think it should be possible.

I don't have much time to look at it, but I'll see if I can build a proof of concept tomorrow.

@kylef That's an awesome idea! I thought using a dictionary in this case might make sense also. Thanks for taking a look!

@kylef Thinking about it a little more, if we override stubForRequest, it would have to somehow be limited to this one stress-test scenario, since I still use other matchers elsewhere, in other test cases.

@abbeycode Okay, what about registering all of the path based matchers in one go.

var paths: [String: NSData] = [:]

func matchPath(request: NSURLRequest) -> Bool {
    let path = request.URL?.path
    return paths[path] != nil
}

func pathBuilder() {
    let path = request.URL?.path
    let body = paths[path]!
    /* get content for path and return it */
}

stub(matchPath, pathBuilder)

This means no changes to Mockingjay itself and you can still use other matchers.

@kylef thanks! I got a working solution using your last suggestion. My stress test went from taking over 2 minutes to under 10 seconds! For reference, this is the full XCTest extension:

import Foundation
import XCTest

import Mockingjay


private var paths = [String:NSData]()

// MARK: - More efficient stubbing for large numbers of mocks whose path is known up front
public extension XCTest {

    func matchPath(request: NSURLRequest) -> Bool {
        let path = request.URL!.path!
        return paths[path] != nil
    }

    func pathBuilder(request: NSURLRequest) -> Response {
        let path = request.URL!.path!
        let body = paths[path]!

        return jsonData(body)(request: request)
    }

    internal func stubByPath(path: String, data: NSData) {
        paths[path] = data
        self.stub(matchPath, builder: pathBuilder)
    }

}