Home > Blockchain >  How to run Xcode tests on data/app (SQLite db, UserDefaults) different from data in default target?
How to run Xcode tests on data/app (SQLite db, UserDefaults) different from data in default target?

Time:10-06

So far I have tested my iOS/Swift project manually by running it in the simulator. Now I have added a Unit Test target to the project to also include automated tests.

I was surprised to find out, that running the default target in Simulator and running the tests target both affect the same data / the same app instance.

For example the uses a SQLite database and UserDefaults to persist some data and settings. When writing data to the database or changing settings in UserDefaults inside the tests, these changes are also visible when running the app in simulator (and vice versa).

I thought because the tests are in a separate target, separate data would be used. This is not the case.

Is it possible to setup the test target to to not interfere with the app target?

CodePudding user response:

Background

That certainly would be a great feature, but it is not possible as of now. This is because a Unit Test is not exactly a full blown iOS App Target. Rather it simply hosts the main App Target as the System Under Testing and tests its code.

Below is a screenshot of the Unit Test Target's "General" Settings tab. See that it is actually hosting the main App Target, and is not a clone / variant of the main App Target.

You can work around this limitation by using the following bit of code which checks whether the application is being Unit Tested.

extension UIApplication{
    
    var isTesting: Bool {
        #if DEBUG
        if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil {
            return true
        }
        return false
        #else
        return false
        #endif
    }
    
}

Note that I have added the "#if DEBUG" conditional compilation markers to prevent process information from being evaluated in release builds.

Below I have presented two workarounds for your scenario.

Workaround for SQLite

You can use this extension to use two different database names depending on whether a Unit Test is being carried out or not.

import SQLite

do {
    let dbName = UIApplication.shared.isTesting ? "db_test" : "db"

    let path = "path/to/\(dbName).sqlite"
    let db = try Connection(path)
    // ...
}
catch{
    print(error)
}

Workaround for UserDefaults

Using two sets of data for normal app execution and testing is not as straightforward. The suggested workaround introduces two new methods similar to the standard setValue and value methods in UserDefaults. These special versions append a "_test" suffix to the key depending on whether a Unit Test is being run or not. The effect is that normal settings are not modified by Unit Test settings.

extension UserDefaults{
    
    private var testKeySuffix: String{
        return UIApplication.shared.isTesting ? "_test" : ""
    }
    
    func safeSetValue(_ value: Any?, forKey key: String){
        UserDefaults.standard.setValue(value, forKey: "\(key)\(testKeySuffix)")
    }
    
    func safeValue(forKey key: String) -> Any?{
        return UserDefaults.standard.value(forKey: "\(key)\(testKeySuffix)")
    }
    
}

The above extension can be used as follows.

// UserDefaults.standard.setValue("123", forKey: "myKey")
UserDefaults.standard.safeSetValue("123", forKey: "myKey")

// let str = UserDefaults.standard.value(forKey: "myKey")
let str = UserDefaults.standard.safeValue(forKey: "myKey")

CodePudding user response:

In addition to the excellent answer by @ThisuraDodangoda I would like to add another solution: Simply create a new target dedicated to the tests

The problem was, that the tests interferes with the data of my app target. So the solution was to simply assign another, new target to the tests:

  • Select the project in the Project Navigator. In my case under "Targets" the app and the test targets are shown.
  • Right click on the app target and select "Duplicate" to create a new target with the same settings.
  • Select the new target from the list and and specify a Bundle Identifier different from the one in the original target.
  • Select the test target from the list select the new app target as "Host Application".
  • A Scheme should have been created automatically for the new app target. Edit it, select the "Test" tab and click on the Button at the bottom of the list and select the tests target to make the the tests available for the new target.
  • Run the tests as usual, they will now use use the new target. The data of the original app target will not change anymore.

Please note, that while this solution works it is not ideal. When new files are added to the project one has always to make sure, that they are not only added to the app target but also to the new target which is used as host app.

  • Related