0

I am saving an Int in UserDefaults and this is reduced by one by clicking a button. I don't know if that is important but I have added an extension to UserDefaults to load an initial value if the app starts for the first time:

extension UserDefaults {

    public func optionalInt(forKey defaultName: String) -> Int? {
        let defaults = self
        if let value = defaults.value(forKey: defaultName) {
            return value as? Int
        }
        return nil
    }
}

The UserDefaults are used as ObservableObject and accessed as EnvironmentObject within the app like this:

class Preferences: ObservableObject {
    @Published var counter: Int = UserDefaults.standard.optionalInt(forKey: COUNTER_KEY) ?? COUNTER_DEFAULT_VALUE {
        didSet {
            UserDefaults.standard.set(counter, forKey: COUNTER_KEY)
        }
    }
}

I am now trying to test that the value in the UserDefaults decreases when the button is clicked. I am trying to read the UserDefaults in the test with:

XCTAssertEqual(UserDefaults.standard.integer(forKey: "COUNTER_KEY"), 9)// default is 10

I have tried it with normal UnitTests where the methods behind the Button are called and with UITests but both do not work. In the UnitTests I get back the COUNTER_DEFAULT_VALUE and in the UiTests I get back 0.

I am trying to access the UserDefaults directly in the test, instead of using the Preferences object, because I have not found a way to access that as it is an ObservableObject.

I have checked in the Emulator that the UserDefaults are saved/loaded correctly when using the app. Is it not possible to access the UserDefaults in the tests or am I doing it wrong?

L3n95
  • 1,505
  • 3
  • 25
  • 49

1 Answers1

2

The key to success is Dependency injection. Instead of directly accessing the shared user defaults object (UserDefaults.standard), declare an object of type UserDefaults within the class:

let userDefaults: UserDefaults

In the view where you declare the model, you are free to use the shared object:

@StateObject var model = Preferences(userDefaults: UserDefaults.standard)

But inside your test, create an dedicated UserDefaults object and pass it to the initializer like so:

let userDefaults = UserDefaults(suiteName: #file)!
userDefaults.removePersistentDomain(forName: #file)
        
let model = Preferences(userDefaults: userDefaults)

The benefit is clear: You control the state of UserDefaults. And that means the code works in every environment. To keep it simple, I haven't incorporated your extension, yet. But I'm sure you will manage to get it working.

TL;DR

Please see my working example below:

ContentView.swift

import SwiftUI
import Foundation

class Preferences: ObservableObject {
    let userDefaults: UserDefaults
    
    @Published var counter: Int {
        didSet {
            self.userDefaults.set(counter, forKey: "myKey")
        }
    }
    
    init(userDefaults: UserDefaults) {
        self.userDefaults = userDefaults
        self.counter = userDefaults.integer(forKey: "myKey")
    }
    
    func decreaseCounter() {
        self.counter -= 1
    }
}

struct ContentView: View {
    @StateObject var model = Preferences(userDefaults: UserDefaults.standard)
    
    var body: some View {
        HStack {
            Text("Value: \(self.model.counter)")
            Button("Decrease") {
                self.model.decreaseCounter()
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

InjectionTests.swift

import XCTest
@testable import Injection

class InjectionTests: XCTestCase {
    func testPreferences() throws {
        // arrange
        let userDefaults = UserDefaults(suiteName: #file)!
        userDefaults.removePersistentDomain(forName: #file)
        
        let model = Preferences(userDefaults: userDefaults)
        
        // act
        let valueBefore = userDefaults.integer(forKey: "myKey")
        model.decreaseCounter()
        let valueAfter = userDefaults.integer(forKey: "myKey")
        
        // assert
        XCTAssertEqual(valueBefore - 1, valueAfter)
    }
}
Nikolai
  • 659
  • 1
  • 5
  • 10