Using Xcode UI tests to test underlying framework behavior - ios

I am building an iOS framework that collects information about various events in an iOS app and performs local and remote analysis. Some of these events can't be tested outside of an app: for example, view controller transitions. To test these events, I built a test iOS app and would like to use Xcode UI tests to:
Initiate a view controller transition by, say, tapping a button that pushes a VC to a navigation controller, or by switching to a different tab in a tab controller
Verify that the framework was able to detect the view controller transitions and generate the necessary events (e.g., send them to the server)
The problem is that Xcode UI tests run outside the app, so I can't access any shared objects to verify that the framework is functioning properly. I also can't insert any mock objects, because, again, I don't have access to the framework loaded in the app's process.
I went as far as trying to load the framework in a kind of test mode and have it update a label with some results that the UI test would then be able to read. But even that is difficult, because XCUIElement doesn't give me the actual text of the element; I can only query for elements with some predefined text. I can sort of work with that, but it seems like a very roundabout approach for something that should be rather trivial.
Are there any better alternatives for testing events that can only be simulated in a running app, and then verifying that my framework was able to detect and process them? In addition to view controller transitions, I am also interested in touch interactions, accelerometer events, and other features that can't be simulated outside of an app. Naturally, I can simulate these events for unit testing, but I would also like to automatically test that when these events occur in an actual app, the proper responses are generated in my framework.

You could try using SBTUITestTunnel. This library extends the functionality of UI Tests adding some features that might come in handy in cases like yours.
Network monitoring
The library allows your test target to collect network calls invoked in the app. Usage is pretty straight forward:
func testThatNetworkAfterEvent() {
// Use SBTUITunneledApplication instead of XCUIApplication
let app = SBTUITunneledApplication()
app.launchTunnelWithOptions([SBTUITunneledApplicationLaunchOptionResetFilesystem]) {
// do additional setup before the app launches
// i.e. prepare stub request, start monitoring requests
}
app.monitorRequestsWithRegex("(.*)myserver(.*)") // monitor all requests containing myserver
// 1. Interact with UI tapping elements that generate your events
// 2. Wait for events to be sent. This could be determined from the UI (a UIActivitiIndicator somewhere in your app?) or ultimately if you have no other option with an NSThread.sleepfortimeinterval
// 3. Once ready flush calls and get the list of requests
let requests: [SBTMonitoredNetworkRequest] = app.monitoredRequestsFlushAll()
for request in requests {
let requestBody = request.request!.HTTPBody // HTTP Body in POST request?
let responseJSON = request.responseJSON
let requestTime = request.requestTime // How long did the request take?
}
}
The nice thing of the network monitoring is that all the testing code and logic is contained in your test target.
Custom block of code
There are other use cases where you need to perform custom code to be conveniently invoked in the test target, you can do that as well.
Register a block of code in the app target
SBTUITestTunnelServer.registerCustomCommandNamed("myCustomCommandKey") {
injectedObject in
// this block will be invoked from app.performCustomCommandNamed()
return "any object you like"
}
and invoke it from the test target
func testThatNetworkAfterEvent() {
let app = ....
// at the right time
let objFromBlock = app.performCustomCommandNamed("myCustomCommand", object: someObjectToInject)
}
Currently you can only inject data from the test target -> app target. If you need the other way around, you could store that data in the NSUserDefaults and fetch it using the SBTUIApplication's userDefaultsObjectForKey() method.
I personally don't like the idea of mixing standard and test code in your app's target, so I'd advise to use this only when really needed.
EDIT
I've update the library and starting from vision 0.9.23 you can now pass back any object from the block to the test target. No need for workarounds anymore!

Not much of an automated way to have things done, but do use breakpoints as an intermediate way, at least until you get things started automatically. You can add a symbolic breakpoint to hit on UI methods such as [UIWindow setRootViewController:], viewWillAppear, etc.
You can explicitly define modules, conditions and actions, so that whenever someone views your VC, some action will be done that might be helpful to you.
If you can print something from your framework using a simple "po myEvent" upon some viewDidAppear, you're good to go. I've never tried using fancier methods as an action, but those can be done as well.

Related

Can I make a background data fetch in an external library?

I am currently making a SDK that would require fetching data in the background. For an example, let's say that the SDK provides some weather data that needs to be relatively fresh to be useful. At some point, the data is outdated and needs to be refreshed. The problem is, that the app might be often used in places with poor internet connection, or with no internet access at all. This brings me to the idea that maybe I should fetch the data in background, when the internet is accessible.
The SDK is packaged into a XCFramework, and distributed using Swift Package Manager. When I try adding capabilities to my target, Xcodes gives me a screen that states "No matches, Capabilities are not supported for SDK".
Is it even possible to make a background data fetch without the access to app capabilities? Or does this responsibility fall to the client app for my SDK? Sorry if the answer is obvious, I've tried searching online for a direct response to my problem, and wasn't able to find a clear answer.
An SDK/framework can't request such capabilities, but it can contain the code necessary to do all the work. You need to expose a function that the client can call to run this code.
E.g.
Inside your framework
public func setupBackgroundDataFetching() {
// create background task
}
Then inside your README, you need to tell users that they have to enable this capability and call:
let weatherSDK = WeatherSDK()
weatherSDK.setupBackgroundDataFetching()
Inside their AppDelegate. Depending on your use case you may need to have the function take in some parameters, or make a singleton class and have this as a static/class func, etc. But the basic idea is the same, wrap up the code and ask the client to invoke it
Example:
Heres the repo of a crash reporting tool I use: https://github.com/getsentry/sentry-cocoa
You'll notice their README contains installation/usage guide which asks users to run a setup method in their AppDelegate, which takes in a configuration. Based on this configuration, they can setup anything they need once the app starts, such as a background task if needed

Where to put network requests when app first starts

When app starts I need to do some network requests. For example, App fetches my servers public key to operate a secure restful connection. If it can get the key, It does other network operations. My problem is that these operations are nested so one is finished, other one start but If there is a problem with one of them I need to display a message. I have AppDelegate class and SplashViewController in which I can do these operations. I'm not sure what will be the best approach in terms of speed when doing that;
1-) Start operations in AppDelegate and with notification, notify splashviewcontroller and display message If there is an error.
2-)Start operations in SplashViewController class.
3-)Wait all network operations to be finished before opening SplashViewController (I'm not sure If I can display error message in AppDelegate class)
Example code I run at AppDelegate;
APIClient.checkCMS { (result) in
switch result{
case .error(let error):
print(error)
//Notify SplashViewController?
case .success(let returnedObject):
print(returnedObject)
print("Devam")
}
}
The fact that you are even thinking about using notifications here to communicate between view controllers is probably a code smell. I think the AppDelegate approach is definitely wrong.
I'm making some assumptions about your app that may be incorrect, namely that you are using a storyboard/xibs and that your SplashVC is going to be instantiated and presented when the app runs, regardless of the state of the network call (i.e. the app isn't just going to hang until the outcome of the network call is known).
In this scenario, if you make the call from the AppDelegate you will use NSNotificationCenter to update the SplashVC. This seems like a bad idea, just because it introduces unnecessary complication to the design. You might just conceivably do everything from the AppDelegate if you are loading the VCs manually in code, but even then you probably don't really want the app to show nothing until the network call completes.
The way I would handle this is as follows:
(If you haven't already done it) encapsulate all of your network requests into a service object of some kind. Then you can use Dependency Injection with a singleton scope (perhaps using Swinject or another similar library). This would allow you to make network requests from anywhere in the app they are required.
Have a default VC (maybe SplashVC, or some other root VC) that loads first and dependency injects the network service from 1
Make the calls in the viewDidLoad of the SplashVC. Handle errors appropriately, (by showing an alert, or by presenting a custom modal VC, or whatever you like. This avoids having the app just show a black screen if the request is slow).
Usually, developers create a fake splash screen. It looks like splash, so user can't notice any difference. On controllers initializer(or in viewDidLoad) you can download all needed data and when everything is done go to next controller. Even more, you can choose where to go next(for example if the user was logged in before, he should be redirected to the main page).

Keeping a WKWebView and it's UIViewController in the background running and accessible from multiple ViewControllers

Background: In order to make web requests to an API endpoint, I need to scrape a website and retrieve a token every 25-30 seconds. I'm doing this with a WKWebView and injecting some custom JavaScript using WKUserScript to retrieve AJAX response headers containing the token. Please focus on the question specifically and not on this background information - I'm attempting this entirely for my own educational purposes.
Goal
I will have different 'model' classes, or even just other UIViewControllers, that may need to call the shared UIViewController to retrieve this token to make an authenticated request.
Maybe I might abstract this into one "Sdk" class. Regardless, this 'model' SDK class could be instantiated and used by any other ViewController.
More info
I would like to be able to call the UIViewController of the WKWebView and retrieve some data. Unless I re-create it every 25 seconds, I need to run it in the background or share it. I would like to be able to run a UIViewController 'in the background' and receive some information from it once WKWebView has done it's thing.
I know there are multiple ways of communicating with another ViewController including delegation and segueing. However, I'm not sure that these help me keep the view containing the WKWebView existing in the background so I can call it's ViewController and have it re-perform the scrape. Delegation may work for normal code, but what about one that must have the view existing? Would I have to re-create this WKWebView dynamically each time a different model, or view controller, were to try and get this token?
One post suggests utilising ContainerViewControllers. From this, I gather that in the 'master' ViewController (the one containing the other ones), I could place the hidden WKWebView to do it's thing and communicate to the child view controllers that way via delegation.
Another post suggests using AppDelegate and making it a shared service. I'm completely against using a Singleton as it is widely considered an anti-pattern. There must be another way, even if a little more complex, that helps me do what I want without resorting to this 'cheat'.
This post talks about communicating between multiple ViewControllers, but I can't figure out how this would be useful when something needs to stay running and executing things.
How about any other ways to do this? Run something in a background thread with a strong pointer so it doesn't get discarded? I'm using Xcode 9.2, Swift 4, and iOS 11. As I'm very new to iOS programming, any small code examples on this would be appreciated.
Unfortunately, WKWebView must be in the view hierarchy to use it. You must have added it as a sub view of an on-screen view controller.
This was fine for me. I added this off-screen so it was not visible. Hidden attribute might have worked as well. Either way you must call addSubview with it to make it work.
There are some other questions and answers here which verify this.
Here is a way if you don't wish to use a singleton.
1- In the DidFinishlaunchingWithOptions, Make a timer that runs in the background and call a method inside the app delegate Called FetchNewToken.
2- In FetchNewToken, make the call needed and retrieve the new token (you can use alamofire or any 3rd library to make the call easier for you).
Up on successfully retrieving the token, save it in NSUserDefaults under the name upToDateToken
You can access this token anywhere from the application using NSUserDefaults and it will always be up to date.

Detect HTTP response using automation instruments in Xcode

I am using the automation instruments in Xcode. I would like to check the response I get when I use a HTTP connection. I don't know if this is possible.
Here is my code:
- (void)readMe
{
[HTTPConnection sendGetToUrl:[NSURL URLWithString:READ_ME_URL]
target:self
selector:#selector(readMeFinished:responseCode:)
failSelector:#selector(downloadFailed:)
userInfo:nil];
}
- (void)readMeFinished:(NSData *)recievedData
responseCode:(NSString *)responseCode
{
//If the response is a 200 OK.
if ([responseCode isEqualToString:#"200"]) {
// HERE, I WOULD LIKE TO DETECT IN AUTOMATION
// THAT I'VE RECEIVED A 200 AND PASS THE TEST
}
}
- (void)downloadFailed:(HTTPConnection *)connection {
// HERE, I WOULD LIKE TO DETECT IN AUTOMATION THAT
// THE DOWNLOAD HAS FAILED AND DON'T PASS THE TEST
}
The automation instrument has no way to reach into your Objective-C code and check for results. It's only access to your application's state is through the user interface.
So, to detect if you have a HTTP 200 status code, you could expose that fact somehow with an invisible label. That would be a brute force way to expose inner application state to the automation instrument.
However!...
I would recommend instead testing for what the user sees if the network connection completes successfully. That's the best way to think about how to use the automation instrument. It's an integration testing tool focused on testing what the user sees and experiences by looking for the state of controls and text on the screen.
You don't provide enough context about what your code actually does in the app, but assuming that the method -readMe shows something to the user that they have to read and approve by clicking a button, I would check to for the result after telling the automation to click the button like a user. What does the user see if there isn't a 200 status code? Do they see an error message? Then to assert that the network code succeeded, check to make sure there isn't an alert window with that error message.
For test automation purposes I think your solution would be better off as a Unit test versus using UI Automation. UI Automation is better used when you have a test case that involves navigating around your application and checking the flow and that expected UI elements are appearing and displaying properly.
If you prefer to stick to UIAutomation, I would recommend maybe creating a function that checks the fields .value() to be not equal to whatever they were initiated as, you can throw this into a while loop with a UIATarget.delay(x) that way you're only checking ever X seconds. I suggest this because then you can add the conditional of if the fields aren't filled then you can throw an error message in your automation test.

ios background download completes but the view has been destroyed

I would like to use AFNetworking to perform periodical JSON requests to my server (updating the user's profile and checking for changes).
What if the background job is running but the user pushed the "Back" button or anything that makes the ViewController to be destroyed? How can I manage this? I mean, in that case I would like to ignore the result and perform it again when the user returns to the View
Thank you
PS: I don't want a full working code. I just would like to know how can I know, from a background download job (ran using AFNetworking) if the ViewController has been destroyed or not.
In order to stop the running network request, you can send cancel to the network operation in method viewWillDisappear: of the View Controller.
Likewise, in order to "automatically" start the network request when the view becomes visible, use method viewWillAppear:.
Check out this question:
Best architecture for an iOS application that makes many network requests?
A simple way is to create a singleton instance of a custom Network Manager class which will handle all the request and network background jobs. You can access it from anywhere (e.g. view controllers) and it keeps its instance for the whole runtime of the app.
About singleton: http://en.wikipedia.org/wiki/Singleton_pattern
If you just need to achieve your task . mean, trying to get the changes if & only if the particular ViewController appears.
Use the methods
-(void) viewDidappear{
// Initialise Your task here
}
-(void) viewWillDisappear{
// Destoy(Cancel ) the task
}
One solution would be to implement the downloading in a way that uses NSNotification mechanism :
Set up the ViewController as a listener.
Set up the bg download to fire up a notification when the download process has been finished.
If the VC is still around when the notification fires, great - it
will handle notification and do whatever you need. If it is not around... nothing happens.

Resources