WeTransfer / Mocker

Mock Alamofire and URLSession requests without touching your code implementation
MIT License
1.1k stars 96 forks source link

mocking "failed" network interactions #24

Closed heckj closed 4 years ago

heckj commented 5 years ago

I didn't see any clear means of returning a failed network interaction (no route to host, for example) with Mocker, but I wanted that for some testing of my own.

I've made a slight modification in a forked version that has an additional initializer ('reportFailure' - a boolean) that will return throw exception on URL access instead of returning Data & a response code. Is this something you'd like as a pull request?

I wanted to offer, but wasn't sure how/if you wanted contributions to this library. I'm using it a bit differently than it was originally designed - more for testing failure scenarios, but it served me pretty well where I needed it: forked variant at https://github.com/heckj/swiftui-notes/blob/master/UsingCombineTests/Mock.swift

AvdLee commented 5 years ago

Any contributions are welcome! As long as it's an optional feature and not hurting anyone else already using the project :)

Feel free to open the PR!

AvdLee commented 4 years ago

Closing this one for inactivity. Feel free to reopen!

martinvw commented 4 years ago

Hello @AvdLee I'm converting some manual mocked tests to your framework, however, I'm a bit surprised to see this failed interactions feature missing.

Is there another way you test error handling? I can not see a way to cover the handling of my certificate pinning errors and some other scenario's such as no connection at all to the server.

I'm BTW using Alamofire 5

Could you share your usage and experience with that?

How could a solution look?

Would the onRequest or completion method be allowed to throw errors or would there be a separate setter which can be assigned an Error to be passed to a call to urlProtocol .. didFailWithError

AvdLee commented 4 years ago

@martinvw you can mock any request with failure response codes and optionally JSON data matching the error just like you would with other mocks:

https://github.com/WeTransfer/Mocker/blob/master/MockerTests/MockerTests.swift#L71

martinvw commented 4 years ago

@AvdLee sorry for the confusion, I was referring to errors other then HTTP-errors, see the example I gave above as well:

I can not see a way to cover the handling of my certificate pinning errors and some other scenario's such as no connection at all to the server.

Or do the frameworks such as Alamofire use other (negative?) status codes for that?

AvdLee commented 4 years ago

@martinvw how would you normally test this? Wouldn't this be out of scope for this framework? Mocker focuses on mocking outgoing network requests. I feel like these are failures happening before a request is going out.

Correct me if I'm wrong!

Otherwise, if you do think the framework should be capable of doing so, we can look into adding the functionality 🙂

martinvw commented 4 years ago

How would you normally test this?

I had the following code to test the URLSession and I added an extension on top of the URLSession to implement the URLSessionProtocol

class MockUrlSession: URLSessionProtocol {
    var capturedRequests = [URLRequest]()
    var mockedData: Data?
    var mockedResponse: URLResponse?
    var mockedError: Error?

    func dataTask(on request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
        capturedRequests.append(request)

        completionHandler(mockedData, mockedResponse, mockedError)

        return MockURLSessionDataTask()
    }
}
...

class MockURLSessionDataTask: URLSessionDataTaskProtocol {
    func resume() {
    }
}

Wouldn't this be out of scope for this framework?

I think that is up to you to decide :-D

I don't think it's out of scope because any solution implementing would be mocking the same classes en doing the same calls.

AvdLee commented 4 years ago

From the above example I don't see anything that is not yet possible. Can you share with me one of your tests that you're currently not able to perform with Mocker?

martinvw commented 4 years ago

Thanks you very much for your reply I will try to come up with a clear verbose example tomorrow. I was busy with some other problems today.

martinvw commented 4 years ago

Thanks for your time!

So it's about setting the mockedError in the snippet above, the original issue starter solved it here:

https://github.com/heckj/swiftui-notes/blob/3226e21389c0a9986eae8cafc40347e9fc5c879f/UsingCombineTests/MockingURLProtocol.swift#L36-L42

My test did, for example, do the following:

func testReportingFailsError() throws {
        mockSession.mockedError = TestError.testError

        errorSubject.report(communication: Communication.init(...], context: context), onSuccess: {
            XCTFail("Expected an error")
        }, onError: {
            self.expectation.fulfill()
        })

        waitForExpectations(timeout: 1.0) { (_) in
            let reported = self.mockErrorLogger.reported
            XCTAssertEqual(1, reported.count)
            XCTAssertEqual("Invalid response received from server: empty", reported.first!.0)
            XCTAssertTrue(reported.first!.1! as? TestError == TestError.testError)
        }
}

And now I also want to create a test which throws the same error as the error thrown when the certificate pinning fails, it could, for example, look like this:

func testReportingFailsToPin() throws {
    var mock = Mock(url: noPinCommunicationUrl, dataType: .json, failure: [.post: AFError.serverTrustEvaluationFailed(reason: Alamofire.AFError.ServerTrustFailureReason.noPublicKeysFound)])
    mock.register()

    errorSubject.report(communication: Communication.init(...)], context: context), onSuccess: {
        XCTFail("Expected an error")
    }, onError: {
        self.expectation.fulfill()
    })

    waitForExpectations(timeout: 10.0) { (_) in
        let reported = self.mockErrorLogger.reported
        XCTAssertEqual(1, reported.count)
        XCTAssertEqual("Failed to validate trust: noPublicKeysFound", reported.first!.0)
        XCTAssertTrue(reported.first!.1! as? TestError == Alamofire.AFError.ServerTrustFailureReason.noPublicKeysFound)
    }
}

Even when configuring the correct pinning in the test alamofire will not respond correctly.

heckj commented 4 years ago

I sort of hacked that on to the side of the Mock, not sure what API would be good/appropriate, so I could use it to generate apparent "failures" within combine flows, and verify the flows were reacting to the failures appropriately. I meant to refactor that out and submit as a PR to Mock so others might have it, but lost track...

heckj commented 4 years ago

@martinvw It's missing some pieces that I'm guessing the project will require (like docs, tests) - but the core of how I was using it is here: https://github.com/WeTransfer/Mocker/pull/52 - refactored to drop over the most recent version of Mocker rather than my earlier hacked/forked copy.

AvdLee commented 4 years ago

I think it's fine to add such a feature if that helps for your cases! I'll go and reply in the PR for the feedback I have 🙂