Stub data for XC UI Tests - ios

So, let me explain my problem first.
I don't want to relay on my web-server data, I want to stub data for my XCUITests.
So, I will be sure that it returns correct data in 100% times, as well as sometimes I need to test some specific(e.g. errors, or empty state) cases, which web-server may not return in exact that moment.
So, what I have tried, it is to run the local server in my XCUITest and then stub some requests, but turns out it didn't work out because XC UI Tests are running in complete separate bundle(even separate process) and local server can't be binded to localhost, so my Main app bundle can't connect to this server.
Another solution that I've tried is to pass some params through XCUIApplication().launchArguments, and based on this params - run stubs on main app, but then - it is a bit of a problem: "I have local-proxy server in main app", which I need only for UI testing.
I know, that also, I can just create stub-server and expose it to the web, so to say, create kind of development-env just for UI tesitng, but it seems to extreme for me. Because in that case maintaining only UI tests for my project is a big effort.
So, my question is, does anyone have better solution? Is there any way to get around this problem without modifying your main app and running external web-server?

You can use SBTUITestTunnel. This library allows to dynamically stub network requests (among other things) in a simple fashion.
The easiest way to add the library is to use cocoapods, then override the initialize method of your AppDelegate:
import UIKit
import SBTUITestTunnel
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
override class func initialize() {
SBTUITestTunnelServer.takeOff()
super.initialize()
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
return true
}
}
Once you added that, you're ready to go. You can add/remove stub to network requests to your UI Tests like in the example below:
func testThatSomethingStubbedWorks() {
let app = SBTUITunneledApplication()
app.launch()
let stubId = app.stubRequestsMatching:SBTRequestMatch(SBTRequestMatch.URL("google.com"), returnJsonDictionary: ["key": "value"], returnCode: 200, responseTime: SBTUITunnelStubsDownloadSpeed3G)
// from here on network request containing 'google.com' will return a JSON {"request" : "stubbed" }
...
app.stubRequestsRemoveWithId(stubId) // To remove the stub either use the identifier
app.stubRequestsRemoveAll() // or remove all active stubs
}

Related

SwiftUI - background processing in a view

I have a music player that needs to fetch the current song from a function over and over again.
up til now I have put the function inside the view (SwiftUI) using the function .onAppear{} while technically it worked I found that when a user clicks on the view to make it bigger, or even when they close and reopen the app from background mode it is re-running the code as if it has not already started.
So I thought by adding a self.timer?.invalidate() function #State var timer: Timer?
I could potentially allow the script to run while it's open and then stop it when it disappears, that worked all the way up to when I needed to add
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification), perform: { output in
self.timer?.invalidate()
})
The issue now is when I reopen the app it won't restart - not sure why.
But I am starting to feel that there must be a better way to do this function, after all it's only checking a function that is in another file on loop.
So my question: where is the best place to create a code that does this
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
//CODE HERE
}
Clearly it needs to run once and in the background but not be called again unless the app is opened for the first time.
I was thinking I need to put it inside the AppDelegate file because I know that file is called only once or when certain functions need to run.
I was thinking this section
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//INSERT CODE HEER
}
I know that I would then need to just store the values to something like
UserDefaults.standard.set
and then just get the MediaPlayerView.swift to refresh these values.
Is this the best practice and what others would recommend?, And am I right by saying a view should only be reading information not trying to run functions.

Why async / long running operations in BackgroundTasks don't work?

Trying to use BackgroundTasks for iOS 13+. Long running operations don't seem to work:
// in AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
BGTaskScheduler.shared.register(forTaskWithIdentifier: "foo.bar.name", using: nil) { task in
print("start!")
task.expirationHandler = {
// Not executed
print("expired!")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
// Not executed
print("finish!")
task.setTaskCompleted(success: true)
}
}
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
BGTaskScheduler.shared.cancelAllTaskRequests()
let request = BGProcessingTaskRequest(identifier: "foo.bar.name")
request.earliestBeginDate = nil // or Date(timeIntervalSinceNow: 0) or Date(timeIntervalSinceNow: 5)...
do {
try BGTaskScheduler.shared.submit(request)
} catch let e {
print("Couldn't submit task: \(e)")
}
}
I also tried using a queue with Operation (for which I modeled my flow synchronously). This also didn't work. As soon as there's something that takes a while to complete, it gets stuck.
It doesn't log anything else to the console, no errors, no expired task message. It shows the last message before the long running operation and that's it. I confirmed that it doesn't move forward by storing a preference and examining it when restarting. It's not stored.
I added "foo.bar.name" to the info.plist (in "Permitted background task scheduler identifiers") and enabled capabilities both for background fetch and background processing. I'm testing on an iPhone with iOS 13.3.1 and using Xcode 11.4.1.
Additional notes:
I've been starting the tasks immediately as described here: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
I also tested with Apple's demo project. It shows the same problem: The database cleaning operation doesn't complete (I added a log at the beginning of cleanDatabaseOperation.completionBlock and it never shows).
A couple of observations:
You should check the result code of register. And you should make sure you didn’t see your “Couldn't submit task” log statement.
Per that discussion in that link you shared, did you set your breakpoint immediately after the submit call? This accomplishes two things:
First, it makes sure you hit that line (as opposed, for example, to the SceneDelegate methods).
Second, if you just pause the app manually, some random amount of time after the app has gone into background, that’s too late. It has to be in that breakpoint immediately after you call submit. Then do e command. Then resume execution.
Anyway, when I do that, running your code, the BGProcessingTaskRequest ran fine. I’m running iOS 13.4.1 (and like you, Xcode 11.4.1).

How to track the time a user listens an specific podcast

I have an iOS app that is about podcasts and I want to track how long a user listens every podcast. I have tried the basic - when a user plays I save the timestamp and when stops it sends an event with the timestamp difference but it obviously doens't work because there's many edge cases.
I have issues to know when a user has the app in background and stops listening at some point through the the system controls. Also when the user or the system kills the app without tapping on "pause" or "stop". I think these 2 cases are my main non-tracked cases so far.
Any idea how can I build a working solution? I don't want/can't pay an external service - I am merely relying on Firebase.
Thanks!
You can override applicationWillTerminate method in your app, and save a current user progress to UserDefaults.
As docs say, you have few seconds to do it:
This method lets your app know that it is about to be terminated and
purged from memory entirely. You should use this method to perform any
final clean-up tasks for your app, such as freeing shared resources,
saving user data, and invalidating timers. Your implementation of this
method has approximately five seconds to perform any tasks and return.
Your code can look like this:
var player: AVPlayer!
func applicationWillTerminate(_ application: UIApplication) {
UserDefaults.standard.setValue(player.currentTime().seconds, forKey: "curPlayerTime")
}
Then, on application launch, you can restore it:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let lastPlayerTime = UserDefaults.standard.value(forKey: "curPlayerTime") as? Double {
// update your player
}
return true
}

Best practice tu run code only once and only when you open an app. Swift 4, Xcode 8.

I am trying to run a function only once only when the app is opened.
Background
I have a stored array (we'll call it storedArray) in user defaults that takes its value from an array that can be modified by the user (userArray).
Problem1: Every time I open the app, storedArray has the values of the previous sessions and of course userArray is empty again. So when the storedArray takes values again from the userArray the storedArray looses its previous values.
Solution to problem1: When you open the app the userArray gets filled with the values of the storedArray (from the previous session).
No problem there. Now:
**Problem 2:**This must happen only once every time the app is opened, otherwise the userArray will start appending the storedArray values many times with repeated values. I figured out solutions 1 and 2, number 3 I tried to follow advice from other posts but failed to understand it and implement it.
Possible Solution 1: Declare a global variable
Struct globalVariables{
static var x = 0
}
Create an if statement so that the desired code runs only if globalVariables.x == 0
if globalVariables.x == 0 {
//code to append values from storedArray to userArray goes here
//Then give a new value to globalVariables.x so that the if never gets accessed again
globalVariables.x == 1
}
This works but I don't know if it as a good practice.
Possible Solution 2: Put the code to be run only once inside the application function in the AppDelegate file.
This functions looks like this.
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//code to append values from storedArray to userArray goes here
}
Again this works but I don't know if it is a good practice.
Possible Solution 3: If I understood correctly, the suggested solution in other posts was to use a singleton. I could never really make this work, (or properly understood what a singleton is).
I made a new .swift file with the following code:
struct Service {
static let sharedInstance = Service(
func getStoredInfo () {
//code to append values from storedArray to userArray goes here
}
}
}
Then call this function in a viewDidLoad with:
Service.sharedInstance.getStoredInfo()
The function still gets called every time the viewDidLoad runs. If the singleton can really help me with this I am totally missing something very basic about what a singleton is and how to implement it. Totally.
Are solutions 1 and 2 good practices? Maybe there are other solutions?
Thanks!
If something need to be done only after launch then 'application didFinishLaunchingWithOptions' is definitely the way to go.
Problem : On iOS you don't control when your app is terminated. You need to store how the app was terminated, so you can restore the session or start a fresh one.

KingFisher + UICollectionView + XCTest with NSURLSession mocking doesn't load images

I'm writing unit tests for some of the views/viewcontrollers in my apps.
My app uses UICollectionView, with the cells containing images loaded using kingfisher. I am using FBSnapshotTestCase to record images of the view and compare them against known good images (and as an aside, using buddybuild's CI to automate running the tests when our developers own pull requests, which is really cool).
I'm using NSURLSession-Mock to insert precanned data (both JSON and images) into the tests.
My problem is that it seems hard to write tests that get the final same end result the users see; I'm frequently finding that (unless the images are already cached - which they aren't as I clear out the cache in test's setup to make sure the tests are running from a clean state!) all the screenshots I take are missing the images, showing only the placeholders.
I've found ways to get this apparently working reliably, but I can't see that I'm 100% happy with my solutions.
Firstly I do this in didFinishLaunchingWithOptions to avoid the application's main UI getting loaded, which caused all sort of confusion when also trying to write tests for the app's home screen:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
BuddyBuildSDK.setup()
//Apply Itison UI Styles
ItIsOnUIAppearance.apply()
#if DEBUG
if let _ = NSClassFromString("XCTest") {
// If we're running tests, don't launch the main storyboard as
// it's confusing if that is running fetching content whilst the
// tests are also doing so.
let viewController = UIViewController()
let label = UILabel()
label.text = "Running tests..."
label.frame = viewController.view.frame
label.textAlignment = .center
label.textColor = .white
viewController.view.addSubview(label)
self.window!.rootViewController = viewController
return true
}
#endif
then in the test, once I've fully setup the UIViewController I need to do things like this:
func wait(for duration: TimeInterval) {
let waitExpectation = expectation(description: "Waiting")
let when = DispatchTime.now() + duration
DispatchQueue.main.asyncAfter(deadline: when) {
waitExpectation.fulfill()
}
waitForExpectations(timeout: duration+1)
}
_ = viewController.view // force view to load
viewController.viewWillAppear(true)
viewController.view.layoutIfNeeded() // forces view to layout; necessary to get kingfisher to fetch images
// This is necessary as otherwise the blocks that Kingfisher
// dispatches onto the main thread don't run
RunLoop.main.run(until: Date(timeIntervalSinceNow:0.1));
viewController.view.layoutIfNeeded() // forces view to layout; necessary to get kingfisher to fetch images
wait(for: 0.1)
FBSnapshotVerifyView(viewController.view)
The basic issue if I don't do this is that KingFisher only starts to load the images when the FBSnapshotVerifyView forces the view to be laid out, and (as KingFisher loads images by dispatching blocks to background threads, which then dispatch blocks back to the main thread) this is too late - the blocks sent to the main thread can't run as the main thread is blocked in FBSnapshotVerifyView(). Without the calls to 'layoutIfNeeded()' and RunLoop.main.run() the KingFisher dispatch_async GCD to the main queue doesn't get to run until the /next/ test lets the runloop run, which is far too late.
I'm not too happy with my solution (eg. it's far from clear why I need to layoutIfNeeded() twice and run the runloop twice) so would really appreciate other ideas, but I hope this at least helps other people that run into the same situation as it took a little bit of head scratching to figure out what was happening.

Resources