Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ApolloStoreSubscriber didChangeKeys with ApolloStore.Activity… #329

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
286 changes: 272 additions & 14 deletions Tests/ApolloTests/Cache/StoreSubscriptionTests.swift
Original file line number Diff line number Diff line change
@@ -1,24 +1,62 @@
import Nimble
import XCTest
@testable import Apollo
//@testable import ApolloSQLite
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//@testable import ApolloSQLite

Doesn't seem like we need this?


class StoreSubscriptionTests: XCTestCase {
open class StoreSubscriptionTests: XCTestCase {
static let defaultWaitTimeout: TimeInterval = 1

var cache: NormalizedCache!
var store: ApolloStore!

override func setUpWithError() throws {
open override func setUpWithError() throws {
try super.setUpWithError()
store = ApolloStore()
cache = InMemoryNormalizedCache()
//cache = try! SQLiteNormalizedCache(fileURL: URL(fileURLWithPath: "/tmp/test.sqlite"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//cache = try! SQLiteNormalizedCache(fileURL: URL(fileURLWithPath: "/tmp/test.sqlite"))

Cleaning this up too. The underlying cache provider shouldn't make a difference in these tests but arguably a memory-based cache would be slightly faster.

store = ApolloStore(cache: cache)
}

override func tearDownWithError() throws {
open override func tearDownWithError() throws {
cache = nil
store = nil
try super.tearDownWithError()
}

// MARK: - Tests

func testUnsubscribeRemovesSubscriberFromApolloStore() throws {
let subscriber = NoopSubscriber()

store.subscribe(subscriber)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a check in between subscribe and unsubscribe to ensure that subscribers is not empty at this point would be beneficial to the test.

store.unsubscribe(subscriber)

expect(self.store.subscribers).toEventually(beEmpty())
}

/// Fufills the provided expectation when all expected keys have been observed.
internal class NoopSubscriber: ApolloStoreSubscriber {

init() {}

func store(_ store: ApolloStore,
didChangeKeys changedKeys: Set<CacheKey>,
contextIdentifier: UUID?) {
// not implemented, deprecated
}

func store(_ store: Apollo.ApolloStore,
activity: Apollo.ApolloStore.Activity,
contextIdentifier: UUID?) throws {
// not implemented
}
}
}

final class StoreSubscriptionSimpleTests: StoreSubscriptionTests {

// MARK: - Tests

func testSubscriberIsNotifiedOfStoreUpdate() throws {
let cacheKeyChangeExpectation = XCTestExpectation(description: "Subscriber is notified of cache key change")
let expectedChangeKeySet: Set<String> = ["QUERY_ROOT.__typename", "QUERY_ROOT.name"]
Expand All @@ -39,16 +77,6 @@ class StoreSubscriptionTests: XCTestCase {
wait(for: [cacheKeyChangeExpectation], timeout: Self.defaultWaitTimeout)
}

func testUnsubscribeRemovesSubscriberFromApolloStore() throws {
let subscriber = SimpleSubscriber(XCTestExpectation(), [])

store.subscribe(subscriber)

store.unsubscribe(subscriber)

expect(self.store.subscribers).toEventually(beEmpty())
}

/// Fufills the provided expectation when all expected keys have been observed.
internal class SimpleSubscriber: ApolloStoreSubscriber {
private let expectation: XCTestExpectation
Expand All @@ -62,10 +90,240 @@ class StoreSubscriptionTests: XCTestCase {
func store(_ store: ApolloStore,
didChangeKeys changedKeys: Set<CacheKey>,
contextIdentifier: UUID?) {
// not implemented, deprecated
}

func store(_ store: Apollo.ApolloStore,
activity: Apollo.ApolloStore.Activity,
contextIdentifier: UUID?) throws {
// To match the old didChangeKeys ApolloStoreSubscriber behavior, only operation on the "did merge" action.
guard case .did(perform: .merge, outcome: .changedKeys(let changedKeys)) = activity else {
return
}
changeSet.subtract(changedKeys)
if (changeSet.isEmpty) {
expectation.fulfill()
}
}
}
}

final class StoreSubscriptionAdvancedTests: StoreSubscriptionTests {

// MARK: - Tests

func testSubscriberIsNotifiedOfStoreRead() throws {
let keys = Set(["QUERY_ROOT"])
let records: RecordSet = [
"QUERY_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
]
]
let _ = try cache.merge(records: records)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"], records.storage["QUERY_ROOT"]!)

let cacheSubscriberExpectation = XCTestExpectation(description: "Subscriber is notified of all expected activities")
let expectedActivitiesSet: Set<ApolloStore.Activity> = [
.will(perform: .loadRecords(forKeys: keys)),
.did(perform: .loadRecords(forKeys: keys), outcome: .records(records.storage))
]
let subscriber = AdvancedSubscriber(cacheSubscriberExpectation, expectedActivitiesSet)

store.subscribe(subscriber)
addTeardownBlock { self.store.unsubscribe(subscriber) }

store.withinReadTransaction { transaction in
try transaction.loadObject(forKey: "QUERY_ROOT").get()
} completion: { result in
switch result {
case .success(let record):
XCTAssertEqual(record, records.storage["QUERY_ROOT"])
case .failure(let error):
XCTFail(String(describing: error))
}
}

wait(for: [cacheSubscriberExpectation], timeout: Self.defaultWaitTimeout)
}

func testSubscriberIsNotifiedOfStorePublish() throws {
let keys = Set(["QUERY_ROOT.__typename", "QUERY_ROOT.name"])
let records: RecordSet = [
"QUERY_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
]
]
let cacheSubscriberExpectation = XCTestExpectation(description: "Subscriber is notified of all expected activities")
let expectedActivitiesSet: Set<ApolloStore.Activity> = [
.will(perform: .merge(records: records)),
.did(perform: .merge(records: records), outcome: .changedKeys(keys)),
]
let subscriber = AdvancedSubscriber(cacheSubscriberExpectation, expectedActivitiesSet)

store.subscribe(subscriber)
addTeardownBlock { self.store.unsubscribe(subscriber) }

store.publish(records: records)

wait(for: [cacheSubscriberExpectation], timeout: Self.defaultWaitTimeout)
}

func testSubscriberIsNotifiedOfStoreRemoveForKey() throws {
let key = "QUERY_ROOT"
let records: RecordSet = [
"QUERY_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
]
]
let _ = try cache.merge(records: records)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"], records.storage["QUERY_ROOT"]!)

let cacheSubscriberExpectation = XCTestExpectation(description: "Subscriber is notified of all expected activities")
let transactionSuccessExpectation = XCTestExpectation(description: "transaction completed successfully")
let expectedActivitiesSet: Set<ApolloStore.Activity> = [
.will(perform: .removeRecord(for: key)),
.did(perform: .removeRecord(for: key), outcome: .success),
]
let subscriber = AdvancedSubscriber(cacheSubscriberExpectation, expectedActivitiesSet)

store.subscribe(subscriber)
addTeardownBlock { self.store.unsubscribe(subscriber) }

store.withinReadWriteTransaction { transaction in
try transaction.removeObject(for: key)
} completion: { result in
switch result {
case .success:
transactionSuccessExpectation.fulfill()
case .failure(let error):
XCTFail(String(describing: error))
}
}

wait(for: [cacheSubscriberExpectation, transactionSuccessExpectation], timeout: Self.defaultWaitTimeout)

XCTAssertNil(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"])
}

func testSubscriberIsNotifiedOfStoreRemoveMatchingPattern() throws {
let pattern = "_ROOT"
let records: RecordSet = [
"QUERY_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
],
"MUTATION_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
]
]
let _ = try cache.merge(records: records)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"], records.storage["QUERY_ROOT"]!)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["MUTATION_ROOT"]))["MUTATION_ROOT"], records.storage["MUTATION_ROOT"]!)

let cacheSubscriberExpectation = XCTestExpectation(description: "Subscriber is notified of all expected activities")
let transactionSuccessExpectation = XCTestExpectation(description: "transaction completed successfully")
let expectedActivitiesSet: Set<ApolloStore.Activity> = [
.will(perform: .removeRecords(matching: "NADA")),
.did(perform: .removeRecords(matching: "NADA"), outcome: .success),
.will(perform: .removeRecords(matching: pattern)),
.did(perform: .removeRecords(matching: pattern), outcome: .success),
]
let subscriber = AdvancedSubscriber(cacheSubscriberExpectation, expectedActivitiesSet)

store.subscribe(subscriber)
addTeardownBlock { self.store.unsubscribe(subscriber) }

store.withinReadWriteTransaction { transaction in
try transaction.removeObjects(matching: "NADA")
}
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"], records.storage["QUERY_ROOT"]!)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["MUTATION_ROOT"]))["MUTATION_ROOT"], records.storage["MUTATION_ROOT"]!)

store.withinReadWriteTransaction { transaction in
try transaction.removeObjects(matching: pattern)
} completion: { result in
switch result {
case .success:
transactionSuccessExpectation.fulfill()
case .failure(let error):
XCTFail(String(describing: error))
}
}

wait(for: [cacheSubscriberExpectation, transactionSuccessExpectation], timeout: Self.defaultWaitTimeout, enforceOrder: true)

XCTAssertNil(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"])
XCTAssertNil(try cache.loadRecords(forKeys: Set(["MUTATION_ROOT"]))["MUTATION_ROOT"])
}

func testSubscriberIsNotifiedOfStoreClear() throws {
let records: RecordSet = [
"QUERY_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
],
"MUTATION_ROOT": [
"__typename": "Hero",
"name": "Han Solo"
]
]
let _ = try cache.merge(records: records)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"], records.storage["QUERY_ROOT"]!)
XCTAssertEqual(try cache.loadRecords(forKeys: Set(["MUTATION_ROOT"]))["MUTATION_ROOT"], records.storage["MUTATION_ROOT"]!)

let cacheSubscriberExpectation = XCTestExpectation(description: "Subscriber is notified of all expected activities")
let cacheClearExpectation = XCTestExpectation(description: "clear cache completed")
let expectedActivitiesSet: Set<ApolloStore.Activity> = [
.will(perform: .clear),
.did(perform: .clear, outcome: .success),
]
let subscriber = AdvancedSubscriber(cacheSubscriberExpectation, expectedActivitiesSet)

store.subscribe(subscriber)
addTeardownBlock { self.store.unsubscribe(subscriber) }

store.clearCache { result in
switch result {
case .success:
cacheClearExpectation.fulfill()
case .failure(let error):
XCTFail(String(describing: error))
}
}

wait(for: [cacheSubscriberExpectation, cacheClearExpectation], timeout: Self.defaultWaitTimeout, enforceOrder: true)

XCTAssertNil(try cache.loadRecords(forKeys: Set(["QUERY_ROOT"]))["QUERY_ROOT"])
XCTAssertNil(try cache.loadRecords(forKeys: Set(["MUTATION_ROOT"]))["MUTATION_ROOT"])
}

/// Fufills the provided expectation when all expected keys have been observed.
internal class AdvancedSubscriber: ApolloStoreSubscriber {
private let expectation: XCTestExpectation
private var activities: Set<ApolloStore.Activity>

init(_ expectation: XCTestExpectation, _ activities: Set<ApolloStore.Activity>) {
self.expectation = expectation
self.activities = activities
}

func store(_ store: ApolloStore,
didChangeKeys changedKeys: Set<CacheKey>,
contextIdentifier: UUID?) {
// not implemented, deprecated
}

func store(_ store: Apollo.ApolloStore,
activity: Apollo.ApolloStore.Activity,
contextIdentifier: UUID?) throws {
activities.remove(activity)
if (activities.isEmpty) {
expectation.fulfill()
}
}
}
}