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
Realmswift + Multiple Predicate
Convert to Latest Swift Syntax' Breaks the Build Even When There Are No Changes
What Does "Constrain to Margins" Mean in Interface Builder in Xcode 6.0.1
Why Does Type(Of:) Return Metatype, Rather Than T.Type
In What Situation Would One Use Expectationfornotification in Swift Testing
Removing a Closure from an Array
Swfitui List Make Scrolling Disabled
iOS Swift Didbegincontact Not Being Called
How to Subclass a Class Which Doesn't Have Any Designated Initializers
Swift Extension Storage for Protocols
How to Make Embedded View Controller Part of the Responder Chain
How to Do a Long Press in Swift
Syncconfiguration Deprecated, What Is the Proper Use of Syncuser.Configuration()