8

I'm testing a class that depends on an instance of UserDefaults. In following this sample code found here, I create and setup the instance like so:

override func setUp() {
    super.setUp()
    defaults = UserDefaults(suiteName: #file)
    defaults.removePersistentDomain(forName: #file)
}

After running the tests, a plist file is created within the same directory as this test class. If my testing file is named TestFile.swift the plist is given the name TestFile.swift.plist. I'm pretty sure this is generated at the call of the suiteName: initializer above. My question is: how do I remove this file once the tests have completed? I've tried making calls to removeSuite(named: #file), removeVolatileDomain(forName: #file), and removePersistentDomain(forName: #file) within the test's tearDown method but no luck. Calling synchronize() didn't seem to help either.

WongWray
  • 2,414
  • 1
  • 20
  • 25

2 Answers2

3

Ideally, instead of passing in an actual instance of UserDefaults, you would instead pass a different type of object that doesn't require that cleanup. A simple example would be something like:

class UnitTestDefaults: UserDefaults {

    private var values = [String: Any]()

    // override only the functions you need, e.g.

    override func object(forKey defaultName: String) -> Any? {
        values[defaultName]
    }

    override func set(_ value: Any?, forKey defaultName: String) {
        values[defaultName] = value
    }

}

Alternatively, you could define a new protocol and have the object you're testing depend on an instance of that protocol instead of UserDefaults. This would be considered more proper practice for unit testing, and would also solve your problem if simply creating an instance of UserDefaults, even a subclass that doesn't define a custom suite, still has unwanted side effects.

protocol DefaultsStorage {
    func object(forKey defaultName: String) -> Any?
    func set(_ value: Any?, forKey defaultName: String)
}

class UnitTestDefaults: DefaultsStorage {
    // ...
}
Sam
  • 125
  • 2
  • 5
Jamie A
  • 881
  • 1
  • 6
  • 14
0

It's a test target so committing it shouldn't be a worry. Once it's committed once there's no need to worry about it again.

Probably a good idea to nil it out on teardown if you want to make sure you leave a clean state between runs.

override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
        try super.tearDownWithError()
        mockDefaults()
    }

Here mockDefaults() calls to pass a value of nil which yields a plist with:

<plist version="1.0">
<dict/>
</plist>

It calls:

private extension FakeInrangeTests {
    func mockDefaults(value: Bool? = nil) -> UserDefaults {
        MockDefaults.makeInrange(#file, value)
    }
}

Which calls:

struct MockDefaults {
    static func make(
        _ file: String,
        value: Any? = nil,
        key: String?
    ) -> UserDefaults {
        let userDefaults = UserDefaults(suiteName: file)!
        userDefaults.removePersistentDomain(forName: file)
        if let value = value, let key = key {
            userDefaults.setValue(value, forKey: key)
        }
        return userDefaults
    }
}

extension MockDefaults {
    static func makeInrange(
        _ file: String,
        _ value: Bool?
    ) -> UserDefaults {
        MockDefaults.make(
            file,
            value: value,
            key: UserDefaults.fakeInrangeKey
        )
    }
}

You can add it to your .gitignore.

*.swift.plist

You can confirm that the plist is not removed by running

var defaults: UserDefaults!

    override func setUpWithError() throws {
        continueAfterFailure = false
        try super.setUpWithError()
        defaults = UserDefaults(suiteName: #file)
        defaults.setValue(12, forKey: "myKey")
    }
    
    override func tearDownWithError() throws {
        try super.tearDownWithError()
        defaults.removePersistentDomain(forName: #file)
    }
SmileBot
  • 19,393
  • 7
  • 65
  • 62
  • Not sure why it should be so complex regarding the cleaning plist file. Wouldn't just a call of userDefaults.removePersistentDomain(forName: file) in tearDown work? Or am I missing something? – Werewolf May 22 '23 at 15:08
  • I updated my post with some sample code you can easily run to test this for yourself @Werewolf – SmileBot May 22 '23 at 17:51