How to Test Asynchronous Methods Swift

How to test a method that contains Task Async/await in swift

Ideally, as you imply, your interactor would be declared using a protocol so that you can substitute a mock for test purposes. You then consult the mock object to confirm that the desired method was called. In this way you properly confine the scope of the system under test to answer only the question "was this method called?"

As for the structure of the test method itself, yes, this is still asynchronous code and, as such, requires asynchronous testing. So using an expectation and waiting for it is correct. The fact that your app uses async/await to express asynchronousness does not magically change that! (You can decrease the verbosity of this by writing a utility method that creates a BOOL predicate expectation and waits for it.)

how to test asynchronous methods Swift

You can achieve that by using XCTestExpectation(since Xcode 6).

How it works:

We create XCTestExpectation instance that work like timer. Your test will never finish till one of both cases happend:

  • XCTestExpectation.fulfill() called
  • you got timeout defined with waitForExpectationsWithTimeout and therefore test will fail

How to use XCTestExpectation

Step 1

Create new protocol for class under test (in your case WmBuildGroupsTask):

protocol MyCallback{
func onDone(results: String)
}

This is our callback.

Step 2

in Unitest inherit this protocol:

class Test_WmBuildGroupsTask : XCTestCase, MyCallback {
/* ...*/
}

Step 3

create XCTestExpectation variable (in Test_WmBuildGroupsTask):

var theExpectation:XCTestExpectation?

and initiate onDone() method:

func onDone(results: String){

theExpectation?.fulfill() // it will release our "timer"
}

Step 4

Example of our test:

func test___WmBuildGroupsTask() {

// Declare expectation
theExpectation = expectationWithDescription("initialized") // dummy text

var task:WmBuildGroupsTask = WmBuildGroupsTask()
task.delegate = self // pass delegate to WmBuildGroupsTask class
task.execute();


// Loop until the expectation is fulfilled in onDone method
waitForExpectationsWithTimeout(500, { error in XCTAssertNil(error, "Oh, we got timeout")
})
}// end func

So now what left us to do is to add some stuff to WmBuildGroupsTask:

Step 5

add new variable:

var delegate:MyCallback?

change onPostExecute method to:

func onPostExecute(transferItem:WmTransferItem){
/* .. */

delegate?.onDone("finished")// call callback
}

Thats all.

( Tested )

Unit test for method that call Async methods

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.

How to test method that dispatch work asynchronously in Swift

Let's assume your test set-up already creates a view controller and calls loadViewIfNeeded() to connect any outlets. (This is from chapter 5, "Load View Controllers.") And that this view controller is in a property I'll name sut (meaning System Under Test).

If you write a test case to call setLabelText(msg:), then immediately check the view controller's label, this won't work.

If you had a dispatch to a background thread, then we'd need the test to wait for the thread to complete. But that's not the case for this code.

Your production code calls setLabelText(msg:) from the background. But test code runs on the main thread. Since it's already on the main thread, all we need to do is execute the run loop one more time. You can express this with a helper function which I introduce in chapter 10, "Testing Navigation Between Screens:"

func executeRunLoop() {
RunLoop.current.run(until: Date())
}

With that, here's a test that works:

func test_setLabelText_thenExecutingMainRunLoop_shouldUpdateLabel() throws {
sut.setLabelText(msg: "TEST")
executeRunLoop()

XCTAssertEqual(sut.label.text, "TEST")
}

This successfully tests the method, and completes quickly. But what if another programmer comes along and changes setLabelText(msg:), pulling the self.label.text = msg call outside the DispatchQueue.main.async? I describe this problem in chapter 13, "Testing Network Responses (and Closures)," in a section called "Keep Asynchronous Code in Its Closure." Basically, we want to test that the label doesn't change when the dispatched closure isn't run. We can do that with a second test:

func test_setLabelText_withoutExecutingMainRunLoop_shouldNotUpdateLabel() throws {
sut.label.text = "123"

sut.setLabelText(msg: "TEST")

XCTAssertEqual(sut.label.text, "123")
}

Swift 5.5 test async Task in init

Expectation-and-wait is correct. You're just using it wrong.

You are way overthinking this. You don't need an async test method. You don't need to call fulfill yourself. You don't need a Combine chain. Simply use a predicate expectation to wait until vm.result is set.

Basically the rule is this: Testing an async method requires an async test method. But testing the asynchronous "result" of a method that happens to make an asynchronous call, like your init method, simply requires good old-fashioned expectation-and-wait test.

I'll give an example. Here's a reduced version of your code; the structure is essentially the same as what you're doing:

protocol Fetching {
func fetch() async -> String
}
class MyClass {
var result = ""
init(fetcher: Fetching) {
Task {
self.result = await fetcher.fetch()
}
}
}

Okay then, here's how to test it:

final class MockFetcher: Fetching {
func fetch() async -> String { "howdy" }
}

final class MyLibraryTests: XCTestCase {
let fetcher = MockFetcher()
func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
let expectation = XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in
subject.result == "howdy"
}), object: nil
)
wait(for: [expectation], timeout: 2)
}
}

Extra for experts: A Bool predicate expectation is such a common thing to use, that it will be found useful to have on hand a convenience method that combines the expectation, the predicate, and the wait into a single package:

extension XCTestCase {
func wait(
_ condition: @escaping @autoclosure () -> (Bool),
timeout: TimeInterval = 10)
{
wait(for: [XCTNSPredicateExpectation(
predicate: NSPredicate(block: { _, _ in condition() }), object: nil
)], timeout: timeout)
}
}

The outcome is that, for example, the above test code can be reduced to this:

    func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
}

Convenient indeed. In my own code, I often add an explicit assert, even when it is completely redundant, just to make it perfectly clear what I'm claiming my code does:

    func testMyClassInit() {
let subject = MyClass(fetcher: fetcher)
wait(subject.result == "howdy")
XCTAssertEqual(subject.result, "howdy") // redundant but nice
}

How to test async functions in swift using XCTWaiter and exceptions

XCTWaiter.wait returns an XCTWaiter.Result which you should be observing.

func testAsyncFunction() {
let expectation = XCTestExpectation(description: "Some description")
vc.asyncFunction(5) { (abc: Int) -> Int in
if (abc != 25) {
// expectation.fulfill()
}
return 0
}

let result = XCTWaiter.wait(for: [expectation], timeout: 2.0) // wait and store the result
XCTAssertEqual(result, .timedOut) // check the result is what you expected
}

Writing Unit Test with asynchronous code in IOS

You can use XCTestExpectation for that.

XCTestExpectation *apiCallExpectation = [self expectationWithDescription:@"APICall"];

[apiService apiCall:^(BOOL success) {
XCTAssert(success);
[apiCallExpectation fulfill];
}];

[self waitForExpectationsWithTimeout:5 handler:^(NSError *error) {
[apiCallExpectation closeWithCompletionHandler:nil];
}];

How to Unit Test asynchronous functions that uses Promise Kit

You seem to have an awful amount of business logic in your view controller, and this is something that makes it harder (not impossible, but harder) to properly test your code.

Recommending to extract all networking and data processing code into the (View)Model of that controller, and expose it via a simple interface. This way your controller becomes as dummy as possible, and doesn't need much unit testing, and you'll be focusing the unit tests on the (view)model.

But that's another, long, story, and I deviate from the topic of this question.

The first thing that prevents you from properly unit testing your function is the APIService.Chart.getVantage(stockId: stockId), since you don't have control over the behaviour of that call. So the first thing that you need to do is to inject that api service, either in the form of a protocol, or in the form of a closure.

Here's the closure approach exemplified:

class MyController {
let getVantageService: (String) -> Promise<MyData>

func getVantage(stockId: String) {
firstly {
self.view.showLoading()
}.then { _ in
getVantageService(stockId)
}.compactMap {
return $0.dataModel()
}.done { [weak self] data in
// same processing code, removed here for clarity
}.ensure {
self.view.hideLoading()
}.catch { [weak self] error in
guard let self = self else { return }
self.handleError(error: error)
}
}
}

Secondly, since the async call is not exposed outside of the function, it's harder to set a test expectation so the unit tests can assert the data once it knows. The only indicator of this function's async calls still running is the fact that the view shows the loading state, so you might be able to make use of that:

let loadingPredicate = NSPredicate(block: { _, _ controller.view.isLoading })
let vantageExpectation = XCTNSPredicateExpectation(predicate: loadingPredicate, object: nil)

With the above setup in place, you can use expectations to assert the behaviour you expect from getVantage:

func test_getVantage() {
let controller = MyController(getVantageService: { _ in .value(mockedValue) })
let loadingPredicate = NSPredicate(block: { _, _ !controller.view.isLoading })
let loadingExpectation = XCTNSPredicateExpectation(predicate: loadingPredicate, object: nil)

controller.getVantage(stockId: "abc")
wait(for: [loadingExpectation], timeout: 1.0)

// assert the data you want to check
}

It's messy, and it's fragile, compare this to extracting the data and networking code to a (view)model:

struct VantageDetails {
let stockValue: Float
let valueIncrease: Float
let percentageIncrease: Float
let roundedPercentageIncrease: String
}

class MyModel {
let getVantageService: (String) -> Promise<VantageDetails>

func getVantage(stockId: String) {
firstly {
getVantageService(stockId)
}.compactMap {
return $0.dataModel()
}.map { [weak self] data in
guard let self = self else { return }
return VantageDetails(
stockValue: Float(data.price ?? "") ?? 0.00,
valueIncrease: Float(data.delta ?? "") ?? 0.00,
percentageIncrease: Float(data.deltaPercentage ?? "") ?? 0.00,
roundedPercentageIncrease: String(format: "%.2f", self.percentageIncrease))
}
}
}

func test_getVantage() {
let model = MyModel(getVantageService: { _ in .value(mockedValue) })
let vantageExpectation = expectation(name: "getVantage")

model.getVantage(stockId: "abc").done { vantageData in
// assert on the data

// fulfill the expectation
vantageExpectation.fulfill()
}

wait(for: [loadingExpectation], timeout: 1.0)
}



Related Topics



Leave a reply



Submit