How To Unit Test an #IBAction with asyncronous call - ios

In the following code, I want to test whether "DisplayHelper.displayAlert" has been called. I am dependency-injecting DisplayHelper and AuthService mock objects, and using PromiseKit
In My ViewController:
#IBAction func submitButtonPressed(sender: AnyObject) {
AuthService.updateUser(["zipcode": self.zipcodeTextField.text!])
.then { user -> Void in
// sucess code
}.error { error -> Void in
self.DisplayHelper.displayAlert("Zipcode Not Found", message: "We can't find that zipcode... please try again.", callingViewController: self)
}
}
Here are the not-working tests:
func testSubmitButtonServerCantFindZipcode() {
vc.zipcodeTextField.text = "00000"
vc.submitButtonPressed(self)
// called AuthService (passes)
XCTAssertEqual(authServiceMock.updateUserCalled, true)
// called displayAlert (fails because tests run before DisplayAlert is called)
XCTAssertEqual(displayHelperMock.displayAlertCalled, true)
}
How do I get the tests to wait for all of the code to execute before the assertions?

When testing asynchronous code with XCTest you need to use XCTestExpectation.
You can rewrite the test code like this:
let e = expectationWithDescription("display alert")
waitForExpectationsWithTimeout(3) { error in
XCTAssertEqual(displayHelperMock.displayAlertCalled, true)
}
Now the only missing piece to make the test work is finding a place where to call expectation.fulfill(). The most appropriate place would be in your AuthService mock, after the success and failure callbacks are run.
If I can suggest you something though, writing tests that asser whether certain methods have been called is not a safe approach to testing, as you are just testing implementation rather than behaviour.
What could be a better approach is to test the two componets independently. TestAuthService making sure that both the success and failure path execute as expected, and the DisplayHelper to make sure that the alert view is actually added to the view hierarchy.
This article could be a useful place to start to find out how unit test alerts, and this post is a good read on why and how to avoid mocks.

Related

What is the appropriate strategy for using #MainActor to update UI?

Suppose you have a method that executes asynchronously in a global context. Depending on the execution you need to update the UI.
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
view.setUser(user)
} catch {
if let error = error {
view.showError(message: error.message)
}
}
}
Where is the correct place to switch to the main thread?
Assign #MainActor to the fetchUser() method:
#MainActor
private func fetchUser() async {
...
}
Assign #MainActor to the setUser(_ user: User) and showError(message: String) view's methods:
class SomePresenter {
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
await view.setUser(user)
} catch {
if let error = error {
await view.showError(message: error.message)
}
}
}
}
class SomeViewController: UIViewController {
#MainActor
func setUser(_ user: User) {
...
}
#MainActor
func showError(message: String) {
...
}
}
Do not assign #MainActor. Use await MainActor.run or Task with #MainActor instead to run setUser(_ user: User) and showError(message: String) on the main thread (like DispatchQueue.main.async):
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
await MainActor.run {
view.setUser(user)
}
} catch {
if let error = error {
await MainActor.run {
view.showError(message: error.message)
}
}
}
}
Option 2 is logical, as you are letting functions that must run on the main queue, declare themselves as such. Then the compiler can warn you if you incorrectly call them. Even simpler, you can declare the class that has these functions to be #MainActor, itself, and then you don't have to declare the individual functions as such. E.g., because a well-designed view or view controller limits itself to just view-related code, it is safe for that whole class to be declared as #MainActor and be done with it.
Option 3 (in lieu of option 2) is brittle, requiring the app developer to have to remember to manually run them on the main actor. You lose compile-time warnings should you fail to do the right thing. Compile-time warnings are always good. But WWDC 2021 video Swift concurrency: Update a sample app points out that even if you adopt option 2, you might still use MainActor.run if you need to call a series of MainActor methods and you might not want to incur the overhead of awaiting one call after another, but rather wrap the group of main actor functions in a single MainActor.run block. (But you might still consider doing this in conjunction with option 2, not in lieu of it.)
In the abstract, option 1 is arguably a bit heavy-handed, designating a function that does not necessarily have to run on the main actor to do so. You should only use the main actor where it is explicitly needed/desired. That having been said, in practice, I have found that there is often utility in having presenters (or controllers or view models or whatever pattern you adopt) run on the main actor, too. This is especially true if you have, for example, synchronous UITableViewDataSource or UICollectionViewDataSource methods grabbing model data from the presenter. If you have the relevant presenter using a different actor, you cannot always return to the data source synchronously. So you might have your presenter methods running on the main actor, too. Again, this is best considered in conjunction with option 2, not in lieu of it.
So, in short, option 2 is prudent, but is often married with options 1 and 3 as appropriate. Routines that must run on the main actor should be designated as such, rather than placing that burden on the caller.
The aforementioned Swift concurrency: Update a sample app covers many of these practical considerations and is worth watching if you have not already.

Unit test is incorrectly executing code from the launch view controller

I have a simple struct as below which is part of an iOS Application:
struct Country: JSONObject {
let name: String!
let code: String!
static let nameKey = "name"
static let codeKey = "dial_code"
init?(_ json: [String: AnyObject]) throws {
guard let name = json[Country.nameKey] as? String else {
throw JSONError.InvalidTypeForKey(key: Country.nameKey)
}
guard let code = json[Country.codeKey] as? String else {
throw JSONError.InvalidTypeForKey(key: Country.codeKey)
}
self.name = name
self.code = code
}
}
I wrote a unit test to test initialisation of this class:
func testCorrectInitialisationOfCountry() {
let countryDict = [Country.nameKey: "England", Country.codeKey: "+44"]
do {
let country = try Country(countryDict)!
XCTAssert(countryDict[Country.nameKey] == country.name, "The country name does not match")
XCTAssert(countryDict[Country.codeKey] == country.code, "The country code does not match")
}
catch _ {
print("Initialisation of country failed with an exception")
}
}
The problem I am facing is this struct is initialised several times in the viewDidLoad() method in the first view controller of my app.
For some reason, that viewDidLoad() method is being called when I run my tests and generating incorrect code coverage because of that. The picture below shows the unit test stats generated by Xcode.
The numbers "241" should actually be just "1". The other 240 times, that line is being executed from the launch view controller which is not under test.
How can I stop the view controller code from executing?
Thanks in advance.
Unit tests are run inside of the context of your running app.
I think your best bet is to write your app delegate's didFinishLaunchingWithOptions to not bring up the view controller if started by a test (or bring up a simpler one)
This answer offers one way to check if your app is running in a unit test:
https://stackoverflow.com/a/30306450/3937
To stop the automatic loading of the first storyboard, remove it as the launch storyboard and code the loading yourself following this answer:
https://stackoverflow.com/a/16702730/3937
You should use the setUp method (inside your unit test class) which is invoked AFTER the viewDidLoad.
Override this method to customize the initial state for all tests in the test case.
This way, you can reset whatever you need just before your test methods execute.
For example, I needed to reset a singleton that was invoked via a view controller. Note that you can reset between each method via the tearDown method:
called once after each test completes. Override this method to perform any per-test cleanup.
There is a solution for your problem. You can short circuit your app launch as described in link below.
How To: Unit Test Without Running The Entire App

iOS UI Unit Testing (XCode7)

I'm a bit confused with the new UI Unit Testing scheme that apple released in their XCode7 Beta. I think it's an awesome idea, but I have a couple questions.
this is one testing method I have...
func testMetricsProperties() {
// Used some of the metrics for testing for reference
let app = XCUIApplication()
app.scrollViews.descendantsMatchingType(.Unknown).containingType(.StaticText, identifier:"rim").childrenMatchingType(.Button).element.tap()
app.textFields["_XCUI:Secure"].typeText("")
app.typeText("\r")
app.buttons["dash metrics"].tap()
let element = app.descendantsMatchingType(.Unknown).containingType(.Image, identifier:"darkBackground.png").childrenMatchingType(.Unknown).element.childrenMatchingType(.Unknown).elementBoundByIndex(1).childrenMatchingType(.Unknown).element.childrenMatchingType(.Unknown).element
let offPlanRevenue = element.childrenMatchingType(.Unknown).elementBoundByIndex(0).staticTexts["OFF PLAN REVENUE"]
offPlanRevenue.tap()
XCTAssert(offPlanRevenue.exists);
XCTAssertEqual(offPlanRevenue.value as! String, "");
}
However, in the next testing method, it seems that I have to load the entire app again,
let app = XCUIApplication()
app.scrollViews.descendantsMatchingType(.Unknown).containingType(.StaticText, identifier:"im").childrenMatchingType(.Button).element.tap()
app.textFields["_XCUI:Secure"].typeText("")
app.typeText("\r")
app.buttons["dash metrics"].tap()
}
Is there anyway I can avoid this? This can be troublesome if i'm trying to run a full test on an entire suite.
I believe what you are looking for is using the setUp() and tearDown() methods. setUp() gets called before each test method and tearDown() gets called after each test method for a class.
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
Use these to clean up between testing methods back to the app's original state.

Performance testing in Swift using TDD

Just learning Test Driven Development in Swift.I created a class that is subclass of XCTestCase in the "ProjectNameTests" group.
class BasicFunctionTest: XCTestCase {
var values : [Int]?
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// This is an example of a functional test case.
XCTAssert(true, "Pass")
}
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
for i in 1...100000{
println("ok i need to know the time for execution")
}
}
}
}
I want to perform performance testing of these method
func testPerformanceExample() {
// This is an example of a performance test case.
self.measureBlock() {
// Put the code you want to measure the time of here.
for i in 1...100000{
println("ok i need to know the time for execution")
}
}
}
and want to find out the time for execution of the for loop.I run the project on going to Product -> Perform Action - > Test Without Building.
I can see the execution time as 3.50 sec but when i try to set the baseline clicking on the Stroke Area as below image:
I get warning as as
Would you like to update the baseline values for all tests on all
devices? You'll have a chance to review the changes the next time you
commit to source control.
Now when i click on Update the error is as below:
How can i set Baseline of the time for my specific method as Here
Here is my test project :
https://drive.google.com/file/d/0Bzk4QVQhnqKmUzYyclp6ZnJvekE/view?usp=sharing
Can you check whether BasicFunctionTest file Target membership includes your test target. You should be able to check this by selecting the file and navigating to File Inspector (target membership).

XCTAssertThrows stops at breakpoint

I'm writing a test method where I want the SUT to throw an exception when under certain conditions. The code looks like this:
- (void) testCantStartTwice
{
XCTAssertThrows([self.sut start], #"");
}
Now, all is good and the test passes. However, I have Xcode set an Exception Breakpoint for all ObjC exceptions, which is pretty useful when testing out an app in the debugger. As you now, now when I execute my test suite with ⌘U, now it stops at that test and looks like if it's failing, even though it says "Test Succeeded".
Any way of making the breakpoint not stop at that test?
Thanks and all the best
Unfortunately, it does not seem possible to have exception breakpoints ignore anything wrapped in XCTAssertThrows (or now XCTAssertThrowsError).
Workaround 1: Deal with the breakpoints or script around them
When you execute these tests, you can either do the following:
Disable the exception breakpoint.
Hit run every time it hits one of your breakpoints.
Both are not ideal as you'll be doing some manual work.
There are suggestions for how to disable breakpoints for Swift XCTAssertThrowsError assertions here. I have not tested those.
Workaround 2: Return Result<T, Error> instead
My original method looked like this:
struct Truck {
static func makeTruck(numberOfWheels: Int) throws -> Self {
if numberOfWheels < 4 {
throw TruckError.tooFewWheels
}
// ...
return .init()
}
}
// Code example:
let truck: Truck? = try? Truck.makeTruck(numberOfWheels: 1)
// Test example:
XCTAssertThrowsError(try Truck.makeTruck(numberOfWheels: 1))
To work around this problem of breakpoints, I created a duplicate internal function that is more testable (since it won't cause the breakpoint problem) by returning a Result instead of throwing an Error.
struct Truck {
static func _makeTruck(numberOfWheels: Int) -> Result<Truck, Error> {
if numberOfWheels < 4 {
return .failure(TruckError.tooFewWheels)
}
// ...
return .success(.init())
}
static func makeTruck(numberOfWheels: Int) throws -> Self {
switch _makeTruck(numberOfWheels: numberOfWheels) {
case .failure(let error):
throw error
case .success(let value):
return value
}
}
}
// Code example:
let truck: Truck? = try? Truck.makeTruck(numberOfWheels: 1)
// Test example:
XCTAssertEquals(Truck._makeTruck(numberOfWheels: 1), .error(TruckError.tooFewWheels))
Workaround 3: Return T? instead
Finally, if you have a simpler use-case, and your function's failure is quite obvious (e.g. there is only one type of error it can return), consider just returning an optional instead of needing the complexity of the Result type:
struct Truck {
static func makeTruck(numberOfWheels: Int) -> Truck? {
if numberOfWheels < 4 {
return nil
}
// ...
return .init()
}
}
// Code example:
let truck: Truck? = Truck.makeTruck(numberOfWheels: 1)
// Test example:
XCTAssertNil(Truck.makeTruck(numberOfWheels: 1))
Perhaps you could use a Test Failure Breakpoint instead of an Exception breakpoint when you are testing. The wwdc 2013 video on unit testing outlined a pretty good workflow for inspecting test failures. It essentially said:
Set a Test Failure Breakpoint which will allow you to inspect the conditions that caused the failure.
If needed, set a manual breakpoint earlier in the test and rerun so you can step through the statements leading to the failure as usual.
This isn't really a direct answer, but as far as I know, I don't think there is a way to create exceptions to the exception breakpoint. Hope it helps
I had the same issue and I was looking for solution for 2 hours. Probably we can't do anything with that.

Resources