2

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?

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225
  • 2
    You might be able to setup environment variables or pre-run/post-run actions to clear the data before/after running. But no, a test target only contains the instructions to run the tests, it doesn't contain a separate instance of the app and i'm not aware of any means to change how that works. You either need to detect running tests and act on that (like clearing all the data mentioned previously) or you need to mock/stub network requests and databases, create dummy databases etc. and use Dependency injection to pass these instances into your classes – Simon McLoughlin Oct 05 '21 at 15:44
  • You could also setup a CI and have your tests run somewhere else. Then it will only be your test data inside it. Xcode comes with an inbuilt Xcode server. Github comes with free CI for open source projects (github actions) etc – Simon McLoughlin Oct 05 '21 at 15:46

2 Answers2

2

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")
  • 1
    Thank you for the great answer. This helped a lot! In addition I have added another answer with an additional solution: Create a new target, specially dedicated to the test. Maybe this is useful to other. – Andrei Herford Oct 06 '21 at 07:44
  • Thank you for choosing my answer as accepted! I actually tried your approach before answering. The reason I didn't post it was because you manually need to add new files to the duplicated App target. Xcode won't maintain any relationship between the targets after they are created. If that's not a deal breaker then your approach is great! – Thisura Dodangoda Oct 06 '21 at 09:19
  • Good point! I have added this to my answer. – Andrei Herford Oct 06 '21 at 09:24
0

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.

Andrei Herford
  • 17,570
  • 19
  • 91
  • 225