mergesort / Boutique

✨ A magical persistence library (and so much more) for state-driven iOS and Mac apps ✨
https://build.ms/boutique/docs
MIT License
898 stars 43 forks source link

Integration Testing - Suggestion / Protocol #63

Open dentvii opened 2 months ago

dentvii commented 2 months ago

Hello, I would like your suggestion on how to implement integration testing. This is my current implementation of a Repository Class, which caches the data and request data from a cloud service provider. All viewModels are injected with this actor.


actor DataRepository : ObservableObject , DataRepositoryProtocol {   
    @Stored(in:.cacheTrackerStore)           var cacheTracker                 : [CacheObject]
}

The problem: the @Stored property makes the cacheTracker variable a [CacheObject], but also neatly and brilliantly exposes the $cacheTracker publisher. This cannot be implemented on a protocol, as it does not allow for the variable to have property wrappers.

protocol DataRepositoryProtocol: ObservableObject, Actor {
    @MainActor var cacheTracker                 : [CacheObject] { get }
} 

Therefore self.dataRepository.$cacheTracker.$tems is not accessible, which makes impossible to do integration tests that leverage combine and publishers.

Does anyone have any suggestion on how, keeping the actor class code mostly unchanged, while still being able to create a mockData to pass on ViewModels, in order to create integration testing? Thanks as always!

mergesort commented 2 months ago

Hey @dentvii, as I mentioned over email it's unfortunate that Swift has a language-level limitation on using property wrappers in a protocol.

Instead of using a protocol I would suggest creating a struct or class that holds onto your Store, with an initializer that takes has a Store<CacheObject> parameter you would like to use, setting it to your @Stored property like this.

public init(store: Store<CacheObject>) {
    self._cacheTracker = Stored(in: store)
}

If you do that then you should be able to use @Stored and control the mock data by passing in a mock data Store. Hope that helps!

dentvii commented 2 months ago

Thanks. Sadly that didn't resolve the issue I was having (because I was unable to init the proposed actor with a mock store on init.


final actor CacheTrackerStore: ObservableObject {
    @Stored var cacheTracker: [CacheObject]

    init(store: Store<CacheObject>) {
        self._cacheTracker = Stored(in: store)
    }
}
actor DataRepository : ObservableObject , DataRepositoryProtocol {   
    @MainActor @Published        var cacheTracker                 : CacheTrackerStore
}

init() {
        self.cacheTracker = CacheTrackerStore(store: Store<CacheObject>(storage: SQLiteStorageEngine.default(appendingPath: "CacheObject"),
                                                                             cacheIdentifier: \.id
                                                                            ))
    }

This yields a Main actor-isolated property 'cacheTracker' can not be mutated from a non-isolated context; this is an error in Swift 6. Therefore the above would not work on the long run.

My solution was to clear the DataRepository actor on the setup of the integration tests, instead of a MockDataRepository using a protocol. If anyone has a better idea please do share.