Find UIRefreshControl in UI Test - ios

I am wanting to test the existence of a UIRefreshControl inside a UI Test. I define my control all init:
itemRefreshControl.accessibilityIdentifier = "MyRefreshIndicator"
// allow UITest to find the refresh
if let refreshLabel = itemRefreshControl.subviews.first?.subviews.last as? UILabel {
refreshLabel.isAccessibilityElement = true
refreshLabel.accessibilityIdentifier = "MyRefreshLabel"
}
Then in my test case I have tried:
let refreshCtlQuery = NSPredicate(format: "label CONTAINS[c] 'Refreshing'")
let refreshControl = app.staticTexts.containing(refreshCtlQuery)
expectation(for: exists, evaluatedWith: refreshControl, handler: nil)
start.press(forDuration: 0, thenDragTo: finish)
print(app.debugDescription)
waitForExpectations(timeout: 5, handler: nil)
I also tried:
let refreshControl = app.staticTexts["MyRefreshLabel"]
and I tried:
let refreshControl = app.activityIndicators["MyRefreshIndicator"]
In all those cases I can see the test runner perform the drag and I see the refresh control in the UI, but the expectation always fails. It's almost like the test blocks until the refreshing is done and then checks for existence and it's not there. When I print out the view hierarchy, I can't find the UIRefreshControl's label. How best can I test this?

Indeed, while UIRefreshControl does its animation, the tests are hang up saying "Wait for <BUNDLE_IDENTIFIER> to idle".
You can swizzle XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle: to an empty method, so you can bypass this behaviour (based on this answer).
extension XCTestCase {
static var disabledQuiescenceWaiting = false
/// Swizzle `XCUIApplicationProcess.waitForQuiescenceIncludingAnimationsIdle(:)`
/// to empty method. Invoke at `setUpWithError()` of your test case.
func disableQuiescenceWaiting() {
// Only if not disabled yet.
guard Self.disabledQuiescenceWaiting == false else { return }
// Swizzle.
if
let `class`: AnyClass = objc_getClass("XCUIApplicationProcess") as? AnyClass,
let quiescenceWaitingMethod = class_getInstanceMethod(`class`, Selector(("waitForQuiescenceIncludingAnimationsIdle:"))),
let emptyMethod = class_getInstanceMethod(type(of: self), #selector(Self.empty))
{
method_exchangeImplementations(quiescenceWaitingMethod, emptyMethod)
Self.disabledQuiescenceWaiting = true
}
}
#objc func empty() {
return
}
}
Then call at the setup of your test case.
override func setUpWithError() throws {
disableQuiescenceWaiting()
}
You can mark the UIRefreshControl directly (I made this extension for convenience).
extension UIRefreshControl {
func testable(as id: String) -> UIRefreshControl {
self.isAccessibilityElement = true
self.accessibilityIdentifier = id
return self
}
}
// Create the instance like this.
UIRefreshControl().testable(as: "RefreshControl")
So you can nicely assert the existence (get from otherElements).
let refreshControlElement = app.otherElements["RefreshControl"]
XCTAssertTrue(refreshControlElement.waitForExistence(timeout: 1))

Related

How to count how many times all classes are called

I want the user to be able to know how many times they have visited each class. Then add together the totals from each page together to form a group sum. I want to print the total sum in the log file in each of the two view controllers. So just one string should be printed.
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC1")
}
}
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.set(true, forKey: "VC2")
}
}
If you mean visited each view controller, when you say visited each class. Then i'd recommend you do it viewDidAppear.
class YourViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(value + 1, forKey: key)
}
}
To make it simpler, you could use an extension on UIViewController.
extension UIViewController {
func updateVisitCount() {
let key = String(describing: type(of: self))
let count = UserDefaults.standard.value(forKey: key) as? Int ?? 0
UserDefaults.standard.set(count + 1, forKey: key)
}
}
Or, if you need this for every view controller that you create, then you can create a base view controller which you would use everywhere instead of UIViewController.
class BaseViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateVisitCount()
}
}
The most automatic solution would be inject the accounting call in viewDidLoad without replacing the original viewDidLoad.
Here demo purpose i've created a sample Playground
import UIKit
import PlaygroundSupport
extension UIViewController {
#objc dynamic func substitutedViewDidAppear() {
print("This is injected code in view did appear")
substitutedViewDidAppear() // it may look like recursive, but it isn't, actually it calls the original `viewDidAppear` method.
}
class func swizzle() {
let originalMethod = class_getInstanceMethod(UIViewController.self, #selector(viewDidAppear(_:)))
let substitutedMethod = class_getInstanceMethod(UIViewController.self, #selector(substitutedViewDidAppear))
if let originalMethod = originalMethod,
let substitutedMethod = substitutedMethod {
print("swizzled")
method_exchangeImplementations(originalMethod, substitutedMethod)
} else {
print("not swizzled")
}
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let label = UILabel()
label.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
label.text = "Hello World!"
label.textColor = .black
view.addSubview(label)
self.view = view
print("view loaded")
}
}
// Swizzle
UIViewController.swizzle() // call this in #top of didFinishLaunchingWithOptions
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Output:
swizzled
view loaded
This is injected code in view did appear
Now in the substitutedViewDidAppear upper portion inject your counting code as #Rakesha Shastri Suggested, call the updateVisitCount method inside of substitutedViewDidAppear & place the UIViewController.swizzle() in applicationDidFinishLaunchingWithOptions before creating the root window.
Create a static variable. A static variable is a type of class, not object therefore throughout all objects a variable maybe maintained. I think this example may better explain how this works. Click here
In ViewDidLoad method call this function :
func updateVisitingCounter() {
var counter = UserDefaults.standard.integer(forKey: "firstPageCounter")
counter += 1
UserDefaults.standard.set(counter, forKey: "firstPageCounter")
}
You may set declare variables at project scope "outside of classes"
var vc1Count = 0
class oneV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc1Count = vc1Count+1
}
}
var vc2Count = 0
class twoV: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
vc2Count = vc2Count+1
}
}
you can also declare these variables at a common place.
As per your requirements its kind of Analytics on app usage. You can implement in 2 ways
By storing data with screen visit in local DB and show it on Analysis Page or on summery page.
Sample code for storing Screen details in DB:
==> Create your Entity for Screen capture.
ScreenVisit.
==> Store Data with screen name.
let entity = NSEntityDescription.entity(forEntityName: "ScreenVisit", in: context)
let newVisit = NSManagedObject(entity: entity!, insertInto: context)
newVisit.setValue("HomeScreen", forKey: "screenname")
newVisit.setValue("1", forKey: "visited")
do {
try context.save()
} catch {
print("Failed saving")
}
==> Fetch data where you required.
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "ScreenVisit")
//request.predicate = NSPredicate(format: <Your Filter Logic>)
request.returnsObjectsAsFaults = false
do {
let result = try context.fetch(request)
for data in result as! [NSManagedObject] {
print(data.value(forKey: "screenname") as! String)
print(data.value(forKey: "visited") as! String)
}
} catch {
print("Failed")
}
You can use any 3rd party library like Google analytics, Crashlytics for tracking your user actions.
Ref Links :
Firebase iOS analytics
Crashlytics
but as per my experience 2nd way is more convenient and powerful.
All depends on your requirements.
Hope this will helps you to get your user action captured.

XCTest: Can expectationForPredicate fulfill on async variable change?

I'm using expectation(for:evaluatedWith:handler:) to watch a variable in production code for a change, but it's never fulfilled - why?
I'd rather not clutter my production code by adding artificial completion blocks or notifications.
class ProductionClass {
var areWeDone = false
func doSomeStuff() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.areWeDone = true
}
}
}
class Test: XCTestCase {
override func setUp() { }
override func tearDown() { }
func testDoSomeStuff() {
let productionClass = ProductionClass()
let predicate = NSPredicate(format: "areWeDone = %d", true)
let exp = expectation(for: predicate, evaluatedWith: productionClass, handler: nil)
productionClass.doSomeStuff()
let result = XCTWaiter.wait(for: [exp], timeout: 3)
if result != XCTWaiter.Result.completed {
XCTAssert(false, "areWeDone changed but test timeout")
}
}
}
The solution is quite easy - just make sure that the class "ProductionClass" inherits from NSObject and your test will work as expected:
import Foundation
class ProductionClass : NSObject {
var areWeDone = false
func doSomeStuff() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.areWeDone = true
}
}
}
I did a quick research in my tests and notice two things:
1. Like J.D. Wooder correctly says, you should inherit your production class from the NSObject (this is what gives you an opportunity to call obj-c runtime methods and use KeyValue methods).
2. The test doesn't crash anymore but still fails. To fix this mark your areWeDone variable with #objc specifier (so it looks like #objc var areWeDone). In my case, it works.

How to use functions in order

import XCTest
class TestClass: XCTestCase
{
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = true
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
XCUIApplication().launch()
// UIApplication.shared.keyWindow?.layer.speed = 100
UIApplication.sharedApplication().keyWindow?.layer.speed = 100
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
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()
{
}
func testA1_TC8InvalidEmailAndPasswordTC8_A1()
{
let app = XCUIApplication()
let emailIdTextField = app.textFields["Email ID"]
emailIdTextField.tap()
if emailIdTextField.value as! String != ""
{
emailIdTextField.clearAndEnterText(emailIdTextField.value as! String)
}
emailIdTextField.typeText("test#gmail.com")
let passwordSecureTextField = app.secureTextFields["Password"]
passwordSecureTextField.tap()
passwordSecureTextField.typeText("dgbnnnbb")
app.buttons["Login"].tap()
app.buttons["OK"].tap()
}
func testA2_TC9KeepAllFieldBlankTC9_A2()
{
let app = XCUIApplication()
let emailIdTextField = app.textFields["Email ID"]
emailIdTextField.tap()
if emailIdTextField.value as! String != ""
{
emailIdTextField.clearAndEnterText(emailIdTextField.value as! String)
}
app.textFields["Email ID"].tap()
let passwordSecureTextField = app.secureTextFields["Password"]
app.secureTextFields["Password"].tap()
if passwordSecureTextField.value as! String != ""
{
passwordSecureTextField.clearAndEnterText(passwordSecureTextField.value as! String)
}
app.secureTextFields["Password"].tap()
app.buttons["Login"].tap()
app.buttons["OK"].tap()
}
func testTC10ValidUsernameAndInvalidPasswordTC10C()
{
let app = XCUIApplication()
let emailIdTextField = app.textFields["Email ID"]
emailIdTextField.tap()
if emailIdTextField.value as! String != ""
{
emailIdTextField.clearAndEnterText(emailIdTextField.value as! String)
}
emailIdTextField.typeText("gfdedkff#gmail.com")
let passwordSecureTextField = app.secureTextFields["Password"]
passwordSecureTextField.tap()
passwordSecureTextField.typeText("grtegrgrtst")
let moreNumbersKey = app.keys["more, numbers"]
moreNumbersKey.tap()
passwordSecureTextField.typeText("#123")
app.buttons["Login"].tap()
app.buttons["OK"].tap()
}
I have multiple functions in test class. I want to use these functions one by one (synchronously) in order because function one result is function two output or parameter. Can you help me to find-out the solution?
in our project we had a similar issue. You cannot force tests execution order on XCTest. We resolved our problem by a method that describes all steps for each test, then we could execute whole path every time and check only specific points in each test.
To check if it works for example code you provided, I created test app >>here<<. The repository is on GitHub and required a bit of work, so I would be grateful if you could take a look at it. There is a basic form I needed for testing and as little code as I could write to test my solution. The most important part are UI tests, you can see two versions there. One in vanilla XCTest and one refactored to use a library I'm developing with my colleagues - AutoMate. It let me clean up the code.
As I was testing this solution in your case, it came to me that you may be tied with a state persisted between launches. That's why I introduced option which can be passed to the application.
var app: XCUIApplication!
override func setUp()
{
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = true
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
app = XCUIApplication()
app.launchEnvironment["NUMBER_OF_ATTEMPTS"] = "0"
app.launch()
// UIApplication.shared.keyWindow?.layer.speed = 100
// UIApplication.sharedApplication().keyWindow?.layer.speed = 100
}
func testA1_TC8InvalidEmailAndPasswordTC8_A1()
{
setUp(forTest: 1)
// Do Your asserts here
}
func testA2_TC9KeepAllFieldBlankTC9_A2()
{
setUp(forTest: 2)
// Do Your asserts here
}
func testTC10ValidUsernameAndInvalidPasswordTC10C()
{
setUp(forTest: 3)
// Do Your asserts here
}
func setUp(forTest testsCount: Int) {
let credentials = [
("test#gmail.com", "dgbnnnbb"),
("", ""),
("gfdedkff#gmail.com", "grtegrgrtst")
]
let emailIdTextField = app.textFields["Email ID"]
let passwordSecureTextField = app.secureTextFields["Password"]
for (no, (email, password)) in credentials.prefix(upTo: testsCount).enumerated() {
fill(emailIdTextField, with: email)
switch no {
case 0:
fill(passwordSecureTextField, with: password, clear: false)
case 1:
fill(passwordSecureTextField, with: password)
passwordSecureTextField.tap()
case 2:
fill(passwordSecureTextField, with: password, clear: false)
let moreNumbersKey = app.keys["more, numbers"]
moreNumbersKey.tap()
passwordSecureTextField.typeText("#123")
default:
break
}
app.buttons["Login"].tap()
app.buttons["OK"].tap()
}
}
func fill(_ field: XCUIElement, with text: String, clear: Bool = true) {
field.tap()
if clear, let stringValue = field.value as? String, stringValue != ""
{
field.clearAndEnterText(field.value as! String)
}
field.typeText(text)
}
This testing framework that you're using (XCTest provided by Apple) is designed such that tests are independent of one another. This is for very (many) good reasons that is out of scope for this answer.
You can do it but you'll be fighting the framework the whole way and or losing out on key functionality provided to you by Xcode and the XCTest framework.
A quick an easy way to do this, is to just have one test() func. Then write all your individual tests as helper functions and call them sequentially within func test().
So...
func testSequentially() {
assertThisFirst()
assertThisSecond()
}
func assertThisFirst() {
// Some test
}
func assertThisSecond() {
// Some other test
}
Now let me reassert that this is not a good way of running tests. You miss out on the ability to run individual tests. You miss out on a lot of nice GUI in Xcode for inspecting tests. Your tests will be a lot harder to debug when failures inevitably do occur. On and on and on... I wish you the best of luck!
You can try this code:
let randomFunction = [test1(),test2(),test3()]
testMethodA()
testMethodB()
testMethodC()

How can I unit test that a block of code is run on DispatchQueue.main

Caveat - I read the few questions about testing threads but may have missed the answer so if the answer is there and I missed it, please point me in the right direction.
I want to test that a tableView call to reloadData is executed on the main queue.
This should code should result in a passing test:
var cats = [Cat]() {
didSet {
DispatchQueue.main.async { [weak self] in
tableView.reloadData()
}
}
}
This code should result in a failing test:
var cats = [Cat]() {
didSet {
tableView.reloadData()
}
}
What should the test look like?
Note to the testing haters: I know this is an easy thing to catch when you run the app but it's also an easy thing to miss when you're refactoring and adding layers of abstraction and multiple network calls and want to update the UI with some data but not other data etc etc... so please don't just answer with "Updates to UI go on the main thread" I know that already. Thanks!
Use dispatch_queue_set_specific function in order to associate a key-value pair with the main queue
Then use dispatch_queue_get_specific to check for the presence of key & value:
fileprivate let mainQueueKey = UnsafeMutablePointer<Void>.alloc(1)
fileprivate let mainQueueValue = UnsafeMutablePointer<Void>.alloc(1)
/* Associate a key-value pair with the Main Queue */
dispatch_queue_set_specific(
dispatch_get_main_queue(),
mainQueueKey,
mainQueueValue,
nil
)
func isMainQueue() -> Bool {
/* Checking for presence of key-value on current queue */
return (dispatch_get_specific(mainQueueKey) == mainQueueValue)
}
I wound up taking the more convoluted approach of adding an associated Bool value to UITableView, then swizzling UITableView to redirect reloadData()
fileprivate let reloadDataCalledOnMainThreadString = NSUUID().uuidString.cString(using: .utf8)!
fileprivate let reloadDataCalledOnMainThreadKey = UnsafeRawPointer(reloadDataCalledOnMainThreadString)
extension UITableView {
var reloadDataCalledOnMainThread: Bool? {
get {
let storedValue = objc_getAssociatedObject(self, reloadDataCalledOnMainThreadKey)
return storedValue as? Bool
}
set {
objc_setAssociatedObject(self, reloadDataCalledOnMainThreadKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
dynamic func _spyReloadData() {
reloadDataCalledOnMainThread = Thread.isMainThread
_spyReloadData()
}
//Then swizzle that with reloadData()
}
Then in the test I updated the cats on the background thread so I could check if they were reloaded on the main thread.
func testReloadDataIsCalledWhenCatsAreUpdated() {
// Checks for presence of another associated property that's set in the swizzled reloadData method
let reloadedPredicate = NSPredicate { [controller] _,_ in
controller.tableView.reloadDataWasCalled
}
expectation(for: reloadedPredicate, evaluatedWith: [:], handler: nil)
// Appends on the background queue to simulate an asynchronous call
DispatchQueue.global(qos: .background).async { [weak controller] in
let cat = Cat(name: "Test", identifier: 1)
controller?.cats.append(cat)
}
// 2 seconds seems excessive but NSPredicates only evaluate once per second
waitForExpectations(timeout: 2, handler: nil)
XCTAssert(controller.tableView.reloadDataCalledOnMainThread!,
"Reload data should be called on the main thread when cats are updated on a background thread")
}
Here is an updated version of the answer provided by Oleh Zayats that I am using in some tests of Combine publishers.
extension DispatchQueue {
func setAsExpectedQueue(isExpected: Bool = true) {
guard isExpected else {
setSpecific(key: .isExpectedQueueKey, value: nil)
return
}
setSpecific(key: .isExpectedQueueKey, value: true)
}
static func isExpectedQueue() -> Bool {
guard let isExpectedQueue = DispatchQueue.getSpecific(key: .isExpectedQueueKey) else {
return false
}
return isExpectedQueue
}
}
extension DispatchSpecificKey where T == Bool {
static let isExpectedQueueKey = DispatchSpecificKey<Bool>()
}
This is an example test using Dispatch and Combine to verify it is working as expected (you can see it fail if you remove the receive(on:) operator).:
final class IsExpectedQueueTests: XCTestCase {
func testIsExpectedQueue() {
DispatchQueue.main.setAsExpectedQueue()
let valueExpectation = expectation(description: "The value was received on the expected queue")
let completionExpectation = expectation(description: "The publisher completed on the expected queue")
defer {
waitForExpectations(timeout: 1)
DispatchQueue.main.setAsExpectedQueue(isExpected: false)
}
DispatchQueue.global().sync {
Just(())
.receive(on: DispatchQueue.main)
.sink { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
completionExpectation.fulfill()
} receiveValue: { _ in
guard DispatchQueue.isExpectedQueue() else {
return
}
valueExpectation.fulfill()
}.store(in: &cancellables)
}
}
override func tearDown() {
cancellables.removeAll()
super.tearDown()
}
var cancellables = Set<AnyCancellable>()
}

UIMenuController doesn't update menu for first time

I have UITextView on which I want to add highlight as custom menu item. I have registered to following notification UIMenuControllerWillShowMenuNotification.
The method for the notification is something like this:
if textIsHighlighted {
let highlightMenuItem = UIMenuItem(title: "Highlight", action: Selector("highlightText"))
UIMenuController.sharedMenuController().menuItems = [highlightMenuItem]
}
else {
let highlightMenuItem = UIMenuItem(title: "Dehighlight", action: Selector("highlightText"))
UIMenuController.sharedMenuController().menuItems = [highlightMenuItem]
}
Although the first time the menucontroller fails to update even though it executes the part of code. It shows the last value. Where should I write this part of code as I feel that during willShow menuController it's already created and thus fails to update.
Hopefully you've solved this by now, but I've just figured this one out myself:
Other answers have said you can update the menu items by adding it when the UIMenuControllerWillShowMenuNotification is called, but this wasn't working for me (iOS 9, Swift 2).
Instead I implemented the UITextView delegate method: textViewDidChangeSelection and set the relevant menu items there:
func textViewDidChangeSelection(textView: UITextView) {
if self.currentSelectionIsInHighlightedRange() {
self.setUpUnhighlightMenuItem()
} else {
self.setUpHighlightMenuItem()
}
}
private func currentSelectionIsInHighlightedRange() -> Bool {
let allHighlightedRanges = self.document.highlightedRanges()
let selectedTextRange = self.documentView.textView.selectedRange
for range in allHighlightedRanges {
let intersectionRange = NSIntersectionRange(range, selectedTextRange)
if intersectionRange.length > 0 {
return true
}
}
return false
}

Resources