I am writing unit tests for a class in a Swift Package for use with iOS. The class being tested (BleCommunicator) uses Bluetooth but the test in question does not. I cannot run the test because I get the error below as the class under test is instantiated:
"State restoration of CBCentralManager is only allowed for applications that have specified the "bluetooth-central" background mode (NSInternalInconsistencyException)"
The solution for an app would be to add the bluetooth-central entry into the Info.plist file. How can this be handled for a Swift Package?
Thanks in advance for the help!
class BleCommunicatorTests: XCTestCase {
var sut: BleCommunicator!
override func setUpWithError() throws {
try super.setUpWithError()
sut = BleCommunicator()
}
func test_WhenSavingSomething_AFileShallBeCreated() {
...
}
Related
I'm using a class, which is originally written in Swift, that extends NSObject from a third-party XCFramework:
open class ExampleClass: NSObject, Codable { ... }
Since this is a third-party class, I can't modify the original, so I created an extension on top of it to override isEqual(_ object:):
extension ExampleClass {
override open func isEqual(_ object: Any?) -> Bool {
return true
}
}
This is currently compiling and building fine on Xcode 11, but with the same branch on Xcode 12, I'm getting an error that says '#objc' instance method in extension of subclass of 'ExampleClass' requires iOS 13.0.0
I've found a similar issue that seems to imply that functionality can't be overridden in a Swift extension, but I'm trying to understand what's changed since migrating from Xcode 11 to 12. Was there something that changed recently that prevents this from happening? Any thoughts on working around this?
I have added functionality to my project that downloads JSON and compares the version numbers in there with the currently installed app version to determine whether a feature should be enabled or not. However, I am now trying to unit test this and I am not sure how to mock the current app version.
Can I inject a value into the info.plist in a test?
Can I completely mock the info.plist in a test?
Or should I:
Add a function in my class to retrieve the version number from the info.plist file and then mock that function?
On app startup, store the version number in NSUserDefaults and the mock this?
I would definitely go with the function which retrieves version number. This way you can get it from info.plist in production code and mock whatever you want in tests. Additionally you will be able to test the retrieval of app version as well :)
Or even better, create another class which gets the application number and inject instance to the class which downloads JSONs. You'll be then able to mock this however you want.
protocol AppVersionProvider {
func getAppVersion() -> String
}
class JSONDownloader {
private let appVersionProvider: AppVersionProvider
public init(appVersionProvider: AppVersionProvider) {
self.appVersionProvider = appVersionProvider
}
public func downloadJSON() {
if appVersionProvider.getAppVersion() != networkingCallResult.appVersion {
...
}
}
}
There, you can mock AppVersionProvider protocol in test with some stub and use info.plist provider for production.
How can I define global variables according to whether or not Xcode UI Tests are running? I'm trying to do this:
#if UITESTS
let api = StubbedAPI()
#else
let api = RealAPI()
#endif
These are global variables, so I can't call NSProcessInfo.processInfo().environment or NSProcessInfo.processInfo().arguments at file scope.
The UI Testing target runs as a separate process from your application. This means you can't set preprocessor macros in the test target and expect the app to know about them. The only way the tests can communicate with the app is via the two processInfo settings you mention.
Using these is dynamic, whereas your proposed solution is static. However, it is still possible to do what you are trying to do with the tools Apple has given us.
First, create a protocol that both StubbedAPI and RealAPI conform to.
protocol API {
// ... //
}
class RealAPI: API {
// ... //
}
class StubbedAPI: API {
// ... //
}
Next, create a configuration class. This will be used to tell your code which API to use at run time.
struct Config {
var api: APIProtocol {
get {
return UITesting() ? RealAPI() : StubbedAPI()
}
}
}
private func UITesting() -> Bool {
return NSProcessInfo.processInfo().arguments.contains("UI-TESTING")
}
Then, retrieve a reference to an implementation of API via the configuration.
class FooService {
private let api = Config().api;
}
Finally, set the processInfo argument before you launch the app under UI Testing.
class UITests: TestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
app.launchArguments = ["UI-TESTING"]
app.launch()
}
}
The api property will be set to the real API when running production code and the stubbed one under UI Testing.
There are some downsides to this approach. First, you are introducing the actual "stubbed" API to your production code. This has the potential downside of a developer actually using this in production. Second, you are required to create the API protocol to only have one "real" object implement it. Unfortunately, this is the best solution I've come up with given the current state of UI Testing and Swift.
I would like my app to run special code (e.g. resetting its state) when running in UI Testing mode. I looked at environment variables that are set when the app is running from UI Testing and there aren't any obvious parameters to differentiate between the app running normally vs in UI Testing. Is there a way to find out?
Two workarounds that I'm not satisfied with are:
Set XCUIApplication.launchEnvironment with some variable that I later check in the app. This isn't good because you have to set it in the setUp method of each test file. I tried setting the environment variable from the scheme settings but that doesn't propagate to the app itself when running UI Testing tests.
Check for the lack of existence of the environment variable __XPC_DYLD_LIBRARY_PATH. This seems very hacky and might only be working now because of a coincidence in how we have our target build settings set up.
I've been researching this myself and came across this question. I ended up going with #LironYahdav's first workaround:
In your UI test:
- (void)setUp
{
[super setUp];
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = #{#"isUITest": #YES};
[app launch];
}
In your app:
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
if (environment[#"isUITest"]) {
// Running in a UI test
}
#JoeMasilotti's solutions are useful for unit tests, because they share the same runtime as the app being tested, but are not relevant for UI tests.
I didn't succeed with setting a launch environment, but got it to work with launch arguments.
In your tests setUp() function add:
let app = XCUIApplication()
app.launchArguments = ["testMode"]
app.launch()
In your production code add a check like:
let testMode = NSProcessInfo.processInfo().arguments.contains("testMode")
if testMode {
// Do stuff
}
Verified using Xcode 7.1.1.
You can use Preprocessor Macros for this. I found that you have couple of choices:
New Target
Make a copy of the App's target and use this as the Target to be Tested. Any preproocessor macro in this target copy is accessible in code.
One drawback is you will have to add new classes / resources to the copy target as well and sometimes it very easy to forget.
New Build Configuration
Make a duplicate of the Debug build configuration , set any preprocessor macro to this configuration and use it for your test (See screenshots below).
A minor gotcha: whenever you want to record a UI Testing session you need to change the Run to use the new testing configuration.
Add a duplicate configuration:
Use it for your Test:
Swift 3 based on previous answers.
class YourApplicationUITests: 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 = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
let app = XCUIApplication()
app.launchArguments = ["testMode"]
app.launch()
// 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() {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
extension UIApplication {
public static var isRunningTest: Bool {
return ProcessInfo().arguments.contains("testMode")
}
}
Then just call UIApplication.isRunningTest in your code.
I've just added this extension
#available(iOS 9, *)
extension XCUIApplication {
func test(){
launchEnvironment = ["TEST":"true"]
launch()
}
}
So I can just use test() instead of launch()
In Swift 3 you can check for the XCInjectBundleInto key, or something that starts with XC.
let isInTestMode = ProcessInfo.processInfo.environment["XCInjectBundleInto"] != nil
This works in OS X as well.
My solution is almost identical to that of Ciryon above, except that for my macOS Document-based app I had to prepend a hyphen to the argument name:
let app = XCUIApplication()
app.launchArguments.append("-Testing")
app.launch()
...otherwise, "Testing" ends up interpreted as the name of the document to open when launching the app, so I was getting an error alert:
I am new to swift. I am not able to get a callback to centralManagerDidUpdateState:: w/ following in playground (i.e.: i thought initialization would call back into centralManagerDidUpdateState):
import CoreBluetooth
class BTDiscovery:NSObject,
CBCentralManagerDelegate {
func centralManagerDidUpdateState(central: CBCentralManager!) {
println("here")
}
}
var bt = BTDiscovery()
Is Core Bluetooth supporting in the iOS Swift playground?
I tried this for OSX playground and IOBluetooth. This also didn't work. What am I doing wrong?
Thank you.
I think what you're running into is the playground is inherently synchronous while the BlueTooth discovery is asynchronous. To allow it to work you need to add some things to your playground to allow asynchronous operation:
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
Also note that since the iOS playground is run on the simulator, I wouldn't necessarily expect CB to work there at all.
You also have more fundamental problems in that you're not doing anything to actually trigger discovery. You need to create an instance of CBCentralManager and use it to drive the discovery process:
import Cocoa
import XCPlayground
import CoreBluetooth
class BTDiscovery:NSObject, CBCentralManagerDelegate {
func centralManagerDidUpdateState(central: CBCentralManager!) {
println("here")
}
}
var bt = BTDiscovery()
var central = CBCentralManager(delegate: bt, queue: dispatch_get_main_queue())
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
You can only use Core Bluetooth on an actual iOS device; it is not supported in the simulator, and by extension, it is not supported in a Playground as this is also executing on your Mac rather than on an iOS device.
Either XCPSetExecutionShouldContinueIndefinitely nor PlaygroundPage.current.needsIndefiniteExecution will work.
Please keep in mind CoreBluetooth only works on device. Which means currently it won't work on the Playground.
It's possible to use Bluetooth in Swift Playground. Please note you have to use PlaygroundBluetooth. You can find more informations about it here Documentation Framework PlaygroundBluetooth
This is how you can scan using the central.
let managerDelegate: PlaygroundBluetoothCentralManagerDelegate = <# manager delegate instance #>
let manager = PlaygroundBluetoothCentralManager(services: nil)
manager.delegate = managerDelegate
Please note PlaygroundBluetooth is just a subset of CoreBluetooth therefore you can not do everything which is normally possible in CoreBluetooth, but I think it's still great for simple things to have fun with.