Perfect, that fits the bill!
I personally (and sadly) have no experience with Xcode cloud yet, but from what I saw of it that should help you a lot in setting up your CI pipeline.
I'm pretty sure UI tests are what you want then, as they're basically equivalent with something like browser tests you can get done for web-apps by e.g. something like browserstack.
Basically they're blackbox tests that internally rely on the accessibility features iOS provides. That means running such a test (from within Xcode, or using xcodebuild
in some CI pipeline) installs a regular build on the device/simulator as well as a testrunner app. That testrunner app runs the tests, which in turn start and "use" the app via the accessibility features (i.e. it "fakes user input").
Tests identify buttons, etc. on screen via their accessibility identifiers (if they don't have that explicitly it's usually the "normal" thing, like a button's title and so on) or by traversing the view hierarchy. For example, to tap a tab bar button and then ensure the scene you land on shows a specific text element you'd write this:
let app = XCUIApplication() // usually set as a property of the test case class
app.tabBars.buttons["Button title"].tap()
guard app.staticTexts["Some label text or the like"].waitForExistence(timeout: 3.0) else {
XCTFail("Did not transition to expected scene!")
return
}
You can even "record" test actions with Xcode, which autocreates the swift code for this, but in my experience that usually needs tweaking afterwards to make it more readable.
Now to the staging part which is usually super important if your app does any calls to a backend during usage that you need to cover in your tests. You'll have to do some things in your production code to help you with tests (but that doesn't mean adding test-specific branches in code!).
- First, be sure to collect any URL definitions in some configuration file (I use plists) that you can easily switch out during tests (or rather load based on the defined stage).
- If you don't want to accidentally ship any such config files to the AppStore, add them under "Development Assets" in your target's build settings (the key is
DEVELOPMENT_ASSET_PATHS
).
- To let the app start in another stage than PROD, use launch arguments or launch environment variables (I use the latter).
- You can set them:
- In a scheme when just running the app (so you can even develop on DEV or the like) under "Product - Edit Scheme... - Run - Arguments".
- For unit tests relying on testplans, you can define them in the Configuration of the testplan itself.
- During UI tests the testrunner can also define it when starting the app with:
let app = XCUIApplication() // see above
app.launchEnvironment["YOUR_LAUNCH_ENV_KEY"] = "DEV" // example values
app.launch() // during UI tests you manually launch the app anyway, usually in thesetUpWithError method
In the (production) code of your app you can access the environment variable with ProcessInfo.processInfo.environment["YOUR_LAUNCH_ENV_KEY"]
. In my current project I have that abstracted into an enum:
enum EnvironmentKey {
case normal // this is PROD
case develop
case testing
init(launchParam: String?) {
switch launchParam {
case "TESTING":
self = .testing
case "DEV":
self = .develop
default:
self = .normal
}
}
}
For my URL "repository" (in the sense that it's the central place where I get my URLs from in the app) I have then this:
struct URLRepository {
static let environment = EnvironmentKey(launchParam: ProcessInfo.processInfo.environment["YOUR_LAUNCH_ENV_KEY"])
static func plistName(for environment: EnvironmentKey = URLRepository.environment) -> String {
var prefix = String(describing: URLRepository.self) // my plist files are named like this
switch environment {
case .testing:
prefix.append("_TESTING")
case .develop:
prefix.append("_DEVSTAGE")
default:
break
}
return prefix
}
init(repoPlistName: String = URLRepository.plistName(), bundle: Bundle = Bundle.main) {
// ...
}
}
Note that all this is fully testable (with unit tests), there's no compiler directives including different code during tests builds than during release builds. I do have unit tests for all of this and never had any problem with my staging so far.
I use a similar approach when setting up my Core Data storage to use different persistent stores (i.e. files) for different stages (that way I don't mix data if I start the app for a different stage).
I think that should set you up with a general strategy to add UI tests. The tests also allow you to make screenshots, which you could then keep in your CI pipeline's artifacts for later inspection.
As a last aside (and in spite of this being so long already): If you cannot reach your backend from the CI at all (like me) you can still make use of this my mocking it out. I use Telegraph for this. It's a small webserver I run inside my testrunner app that answers with fixed responses for each request the app makes, I just have a url repo file that contains URLs to localhost for this. The only other thing needed for this is an exception entry for ATS in my app's info.plist (I only do that for debug builds), let me know if you need that, too.