Testing
Testing peer-to-peer systems can be challenging, but worth the initial effort to ensure your data model works across multiple instances. It is important to test your business logic with end-to-end unit tests that involve multiple Ditto peers.
A typical test lifecycle is:
- Create two instances of Ditto with the same identity and different directories.
- Call startSync() on both instances.
- Listen for changes to data using subscribe() and write tests as a response to the observeLocal() function.
- Clean up Ditto directories between tests and stop any open synchronization threads using stopSync().
For complete examples and running tests, see the open source samples GitHub repository.
Getting Started
To test Ditto using SwiftUI, we provide you with some basic convenience functions you can use to get started.
For code that you can use in your project, see the example on GitHub.
Create custom directories
Each instance of Ditto should use a different directory. You can create a convenience class to provide a custom directory for every instance of Ditto.
/// Test helper which wraps a Ditto instance.
public class DittoInstance {
internal var ditto: Ditto?
private let instanceDir: URL
public init(appID: String) {
// No need for cleanup, as the TestsSetup class is configured as NSPrincipalClass
// and will delete topLevelDittoDir() before any test job is run
let instanceDir = topLevelDittoDir()
.appendingPathComponent(appID)
.appendingPathComponent(UUID().uuidString)
self.instanceDir = instanceDir
self.ditto = Ditto(
identity: .onlinePlayground(appID: appID, persistenceDirectory: instanceDir),
persistenceDirectory: instanceDir
)
}
public func stop() {
self.ditto!.stopSync()
self.ditto = nil
}
}
func topLevelDittoDir() -> URL {
let fileManager = FileManager.default
return try! fileManager.url(
for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false
).appendingPathComponent("ditto_top_level")
}Create your test
- Initialize two ditto instances with different directories.
- Call startSync() on both instances.
import XCTest
import DittoSwift
final class SwiftUITests: XCTestCase {
var ditto1: DittoInstance!
var ditto2: DittoInstance!
override func setUp() {
self.ditto1 = DittoInstance(appID: "test-app")
self.ditto2 = DittoInstance(appID: "test-app")
try! self.ditto1.ditto?.startSync()
try! self.ditto2.ditto?.startSync()
}
func testExample() throws {
// your test here
}
}Listen for Data Changes
Using observeLocal(), we listen to changes to data. We write tests as a response to the observeLocal() function based on what we expect to happen.
The tests should reside within the callback so that they properly test the state of the database after synchronization is complete.
...
let initialResultExpectation = expectation(description: "Initial event received")
let docID = try! ditto1.ditto!.store.collection("cars").upsert(["make": "toyota", "color": "red"])
let subscription = ditto2.ditto!.store.collection("cars").findAll().subscribe()
let liveQuery = ditto2.ditto!.store.collection("cars").findByID(docID).observeLocal { doc, event in
if (!event.isInitial) {
XCTAssertEqual(doc!.value["make"] as! String, "toyota")
initialResultExpectation.fulfill()
}
}
wait(for: [initialResultExpectation], timeout: 2)Clean up
Don't forget to clean up between tests. This can also be implemented as part of a base test class.
func testExample() throws {
let initialResultExpectation = expectation(description: "Initial event received")
let docID = try! ditto1.ditto!.store.collection("cars").upsert(["make": "toyota", "color": "red"])
let subscription = ditto2.ditto!.store.collection("cars").findAll().subscribe()
let liveQuery = ditto2.ditto!.store.collection("cars").findByID(docID).observeLocal { doc, event in
if (!event.isInitial) {
XCTAssertEqual(doc!.value["make"] as! String, "toyota")
initialResultExpectation.fulfill()
}
}
wait(for: [initialResultExpectation], timeout: 2)
liveQuery.stop()
ditto1.stop()
ditto2.stop()
}Full example
For the full example that you can use in your project, see the open source testing library on GitHub.`