class ProfileVC: BaseUIVC {
var dispatchGroupHelper: DispatchGroupImpl
var isFlag: Bool
init(dispatchGroupHelper: DispatchGroupImpl = DispatchGroupHelper(), isFlag: Bool = false) {
self.dispatchGroupHelper = dispatchGroupHelper
self.isFlag = isFlag
super.init(nibName: nil, bundle: currentBundle())
}
func save() {
print("Outside isFlag : \(isFlag)")
print("Outside self : \(self)")
if let info = getData() {
showAlertOnWindow(message: "message") { (_) in
print("Inside isFlag : \(self.isFlag)")
print("Inside self : \(self)")
if param.count > 0 {
self.dispatchGroupHelper.joinDispatchGroup()
self.requestAPI1(data: param)
}
if isFlag {
self.dispatchGroupHelper.joinDispatchGroup()
self.requestAPI2(data: param)
}
self.dispatchGroupHelper.notifyDispatchGroup(execute: {
self.handleResponse()
})
}
}
}
public func showAlertOnWindow(title: String? = "", message: String? = nil,
completionHandler: ((_ title: String) -> Void)? = nil) {
let alert = UIAlertController(title: title ?? "", message: message,
preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "dummy", style: UIAlertActionStyle.default, handler: { (_) in
if let completionHanlder = completionHandler {
completionHanlder("OK")
}
}))
alert.show()
print("Display alert : \(alert)")
}
}
class DispatchGroupHelper: DispatchGroupImpl {
lazy var group: DispatchGroup? = {
return DispatchGroup()
}()
}
protocol DispatchGroupImpl {
var group: DispatchGroup? { get }
func joinDispatchGroup()
func leaveDispatchGroup()
func notifyDispatchGroup(execute: @escaping () -> Void)
}
extension DispatchGroupImpl {
func joinDispatchGroup() {
group?.enter()
}
func leaveDispatchGroup() {
group?.leave()
}
func notifyDispatchGroup(execute: @escaping () -> Void) {
group?.notify(queue: DispatchQueue.main, execute: execute)
}
}
extension UIViewController {
func show() {
present(animated: true, completion: nil)
}
func present(animated: Bool, completion: (() -> Void)?) {
if let rootVC = UIApplication.shared.keyWindow?.rootViewController {
if #available(iOS 13.0, *), let navVC = rootVC as? UINavigationController,
let visibleVC = navVC.visibleViewController {
print("VisibleVC : \(visibleVC)")
visibleVC.showPrompt(withAlertController: self)
} else {
rootVC.showPrompt(withAlertController: self)
}
}
}
}
public extension UIAlertController {
func tapButton(title: String) {
guard let action = actions.first(where: {$0.title == title}), let block = action.value(forKey: "handler") else {
return
}
let handler = unsafeBitCast(block as AnyObject, to: AlertHandler.self)
handler(action)
}
}
Test Case
class MockDispatchGroupImpl: DispatchGroupImpl {
let group: DispatchGroup?
var joinCount = 0
var leaveCount = 0
var isNotifyCalled = false
init(group: DispatchGroup) {
self.group = group
}
func joinDispatchGroup() {
joinCount += 1
print("Join: \(joinCount)")
}
func leaveDispatchGroup() {
leaveCount += 1
print("Leave")
}
func notifyDispatchGroup() {
isNotifyCalled = true
print("Notify")
}
}
class ProfileTests: XCTestCase {
func testAPIWhenIsFlagTrue() {
var mockDispatchGroupHelper: MockDispatchGroupImpl? = MockDispatchGroupImpl(group: DispatchGroup())
var profilerVC = getProfileVC(dispatchGroupHelper: mockDispatchGroupHelper!, isFlag: true)
profilerVC?.save()
let alert = UIApplication.shared.keyWindow?.rootViewController?.presentedViewController as! UIAlertController
alert.tapButton(title: "OK")
print("Capture Alert : \(alert)")
print("TAP")
XCTAssertEqual(mockDispatchGroupHelper?.joinCount, 2)
}
func testAPIWhenIsFlagFalse() {
var mockDispatchGroupHelper: MockDispatchGroupImpl? = MockDispatchGroupImpl(group: DispatchGroup())
var profilerVC = getProfileVC(dispatchGroupHelper: mockDispatchGroupHelper!, isFlag: false)
profilerVC?.save()
let alert = UIApplication.shared.keyWindow?.rootViewController?.presentedViewController as! UIAlertController
alert.tapButton(title: "OK")
print("Capture Alert : \(alert)")
print("TAP")
XCTAssertEqual(mockDispatchGroupHelper?.joinCount, 1)
}
}
Running tests individually are passing. But when all tests are run in ProfileTests
testAPIWhenIsFlagTrue: PASS
testAPIWhenIsFlagFalse: FAIL
"XCTAssertEqual failed: ("Optional(0)") is not equal to ("Optional(1)")"
Debug Result
["testAPIWhenIsFlagTrue"]
Outside isFlag : true
Outside self: <ProfileVC: 0x7fdd13818600>
VisibleVC : <SplashViewController: 0x7fdd123023d0>
**Display alert : <UIAlertController: 0x7fdd1282a000>**
**["Capture Alert : <UIAlertController: 0x7fdd1282a000>"]**
["TAP"]
Inside isFlag : true
Inside self: <ProfileVC: 0x7fdd13818600>
["Join: 1"]
["Join: 2"]
["Notify"]
["testAPIWhenIsFlagFalse"]
Outside isFlag : false
Outside self: <ProfileVC: 0x7fdd10242a00>
**VisibleVC : <UIAlertController: 0x7fdd1282a000>)**
Display alert : <UIAlertController: 0x7fdd1282a600>
**["Capture Alert : <UIAlertController: 0x7fdd1282a000>"]**
["TAP"]
Inside isFlag : true
Inside self: <ProfileVC: 0x7fdd13818600>
["Join: 3"]
["Join: 4"]
["Notify"]
XCTAssertEqual failed: ("Optional(0)") is not equal to ("Optional(1)")
Observation:
<UIAlertController: 0x7fdd1282a000>)
Same alert capture for both tests .
Join Count increasing to 4
In function
tapButton()
we are calling the alert action handler, but not dismissing the actual alert. How to dismiss alert in XCTestCase? Note:- showAlertOnWindow() is in Utility class and has been used by various classes.
The safest way to unit test alerts is to have them not actually appear. With the alert recorded but not shown:
To that end, try using https://github.com/jonreid/ViewControllerPresentationSpy. In test code, instantiate an
AlertVerifier
. Then callverify()
to confirm its appearance and other attributesexecuteAction(forButton:)
to call the closure for a particular buttonThere's no need to dismiss anything, because a real alert isn't presented when an
AlertVerifier
exists.