Nandan
Nandan

Reputation: 55

How to write unit tests for methods in swift that returns promises with response obtained by invoking mocked class methods?

I am writing unit tests for methods of a class in swift. These methods invoke class methods that are mocked for unit test. Methods return promises with response obtained from the mocked static/class methods.

NetworkAuthProtocol:-

protocol NetworkAuthProtocol {
      static func httpGet(_ url: String, completionHandler complete: @escaping(Any?, Error?) -> Void)
      static func httpDelete(_ url: String, completionHandler complete: @escaping (Any?, Error?) -> Void)
}

Below is the mocked class for unit test with class methods:-

class NetworkAuthMock: NetworkAuthProtocol {

    class func httpGet(_ url: String, completionHandler complete: @escaping(Any?, Error?) -> Void) {
        complete(nil, Error)
    }

    class func httpDelete(_ url: String, completionHandler complete: @escaping (Any?, Error?) -> Void) {
        complete(nil, Error)
    }
}

Below is the class for which I am writing unit tests:-

class FetchDataAPIService {

    var networkAuthDelegate: NetworkAuthProtocol.Type = NetworkAuth.self

    func fetchUserData(userUrl: String) -> Promise<Any> {
        return Promise<Any> { seal in
            networkAuthDelegate.httpGet(userUrl) { (result, error) in
                if error == nil {
                    seal.fulfill(result!)
                } else {
                    seal.reject(error!)
                }
            }
        }
    }

   func deleteUserData(userUrl: String) -> Promise<Any> {
        return Promise<Any> { seal in
            networkAuthDelegate.httpDelete(userUrl) { (_, error) in
                if error == nil {
                    seal.fulfill(())
                } else {
                    seal.reject(error!)
                }
            }
        }
    }
}

Unit tests I have written, adding it below:-

class FetchDataAPIServiceTests: XCTestCase {
    var fetchDataAPIService: FetchDataAPIService!
    var result: String?
    var error: Error?

    override func setUp() {
        super.setUp()
        self.fetchDataAPIService = FetchDataAPIService()
        self.fetchDataAPIService.networkAuthDelegate = NetworkAuthMock.self
        self.error = nil
    }

    override func tearDown() {
        super.tearDown()
    }

    func testFetchUserData() {

        let expectation = XCTestExpectation(description: "test fetchUserData")
        firstly {
            self.fetchDataAPIService.fetchUserData(userUrl: "url")
        }.done { (_) in
            XCTFail("test failed")
        }.catch { (error) in
            XCTAssertNotNil(error)
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 10.0)
    }

    func testDeleteUserData() {

        let expectation = XCTestExpectation(description: "test deleteUserData")
        firstly {
            self.fetchDataAPIService.deleteUserData(userUrl: "url")
        }.done { (_) in
            XCTFail("test failed")
        }.catch { (error) in
            XCTAssertNotNil(error)
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 10.0)
    }
}

With expectations, the test pass in isolation but together it fails. I have tried other methods like adding DispatchQueue.main.asyncAfter outside XCTAssert statement. Also I added breakpoints in httpGet and httpDelete method of Mock class, the control never reaches there. Nothing seems to work. Code coverage works only when tests are run in isolation but that too sometimes and also test build crashes in main.m file. How can I make all tests succeed and also cover test coverage for all the methods?

Upvotes: 0

Views: 1474

Answers (2)

Nandan
Nandan

Reputation: 55

As both done/catch block and also waitForExpectation run in main queue, done/catch block waits for waitForExpectation method to complete and vice versa which results in a deadlock. So, by running done and catch block in background queue like below, tests should succeed.

func testFetchUserData() {

        let expectation = XCTestExpectation(description: "test fetchUserData")
        firstly {
            self.fetchDataAPIService.fetchUserData(userUrl: "url")
        }.done(on: .global(qos: .background)) { (_) in
            XCTFail("test failed")
        }.catch(on: .global(qos: .background)) { (error) in
            XCTAssertNotNil(error)
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 10.0)
 }

Upvotes: 1

Cristik
Cristik

Reputation: 32925

See, that's one of the problem of static functions - you need to make sure you do the proper cleanup before/after each test, to make sure you don't inflict implicit dependencies between the tests.

I'd recommend instead to switch to a more OOP approach by using instance methods:

protocol NetworkAuthProtocol {

    func httpGet(_ url: String, completionHandler complete: @escaping(Any?, Error?) -> Void)

    func httpDelete(_ url: String, completionHandler complete: @escaping (Any?, Error?) -> Void)
}


class FetchDataAPIService {

    let networkAuthDelegate: NetworkAuthProtocol

    init(networkAuth: NetworkAuthProtocol) {
        self.networkAuthDelegate = networkAuth
    }

, and in your tests add support for configuring which response to send:

class TestsNetworkAuth {
    var httpGetResult: (Any?, Error?) = (nil, nil)
    var httpDeleteResult: (Any?, Error?) = (nil, nil)

    func httpGet(_ url: String, completionHandler complete: @escaping(Any?, Error?) -> Void) {
        complete(httpGetResult.0, httpGetResult.1)
    }

    func httpDelete(_ url: String, completionHandler complete: @escaping (Any?, Error?) -> Void) {
        complete(httpDeleteResult.0, httpDeleteResult.1)
    }
}

class FetchDataAPIServiceTests: XCTestCase {
    var testsNetworkAuth = TestsNetworkAuth()
    var fetchDataAPIService: FetchDataAPIService!

    override func setUp() {
        super.setUp()
        fetchDataAPIService = FetchDataAPIService(networkAuth: testsNetworkAuth)
    }

With the above approach you can add all kind of tests simulating various result data and/or error. And the tests will not interfere one with another since each has his own instance of TestsNetworkAuth. In the original approach due to the static nature of the methods, the tests were sharing data.

Upvotes: 1

Related Questions