Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit test for method that call Async methods

supposed I have these lines of code:

func reset() {
    initializeAnythingElse() {
        // AnythingElse
    }

    initializeHomeData() {
        // HomeData
    }
}

func initializeHomeData(callback: @escaping (()-> Void)) {
    getHomeConfig() {
        callback()
    }
}

func initializeAnythingElse(callback: @escaping (()-> Void)) {
    getAnythingElse() {
        callback()
    }
}

and I would like to write a unit test for that code. For initializeHomeData and initializeAnythingElse, I can write the unit test like :

func testInitializeHomeData() {
    let successExpectation = expectation(description: "")

    sut.initializeHomeData {
        successExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)

    // Validation goes here
}

func testInitializeAnythingElse() {
    let successExpectation = expectation(description: "")

    sut.initializeAnythingElse {
        successExpectation.fulfill()
    }

    waitForExpectations(timeout: 1.0, handler: nil)

    // Validation goes here
}

My question is, how to test reset()? Should I just call them inside testReset() like :

func testReset() {
    testInitializeHomeData()
    testInitializeAnythingElse()
}

but I think this is not the proper implementation for that.

like image 906
mrjimoy_05 Avatar asked Jul 17 '17 03:07

mrjimoy_05


People also ask

Can unit tests be async?

Async unit tests that return Task have none of the problems of async unit tests that return void. Async unit tests that return Task enjoy wide support from almost all unit test frameworks.

Which method calls the async task?

A synchronous method calls an async method, obtaining a Task . The synchronous method does a blocking wait on the Task .


1 Answers

You are right. To test reset you need to call reset, and not it's internal methods.

That being said, reset is currently written in a way that makes it untestable. The reason you are able to test the other standalone methods so easily is because of the callback argument both accepts.

I would recommend you rewrite reset to allow two optional callbacks as follows:

typealias Callback = () -> ()

func reset(
    homeDataCallback: @escaping Callback? = nil,
    anythingElseCallback: @escaping Callback? = nil) {
    initializeAnythingElse() {
       anythingElseCallback?()
    }
    initializeHomeData() {
       homeDataCallback?()
    }
}

Note that this change allows you get notified, in async, when those two internal calls complete.

Now, your test method needs to be written with some sort of synchronization primitive in mind, since logically, reset is only complete when both home data and anything else is done and their callbacks invoked.

There are many ways to achieve this, but I will show you an approach with semaphores:

func testReset() {

    let expectation = expectation(description: "reset() completes within some duration")

    // some mechanism to synchronize concurrent tasks
    // I am using a semaphore
    let s = DispatchSemaphore(value: 0)

    let homeCallback: Callback =  {
        s.signal() // signals the completion of home data init
    }

    let anythingElseCallback: Callback = {
        s.signal() // signals the completions of anything else setup
    }

    // call your reset method as part of the test
    reset(homeDataCallback: homeCallback, anythingElseCallback: anythingElseCallback)

    // we know we need to wait for two things to complete
    // init home data and anything else, so do that
    s.wait()
    s.wait()

    // at this step, reset's internal async methods
    // have completed so we can now
    // fulfill the expectation
    expectation.fulfill()

}

Note that all this change is required to purely allow you to test the reset call. Your function signature allows you to write reset() as current in your existing code since it has optional arguments that are both set to nil for default values.

like image 129
Benzi Avatar answered Oct 05 '22 14:10

Benzi