feat: Working on mocks and mock storage.

This commit is contained in:
2025-01-16 16:29:46 -05:00
parent 94b2b1e50c
commit b6e7fe915f
7 changed files with 397 additions and 17 deletions

View File

@@ -21,7 +21,7 @@ let package = Package(
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"), .package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.6.0"),
// 🍃 An expressive, performant, and extensible templating language built for Swift. // 🍃 An expressive, performant, and extensible templating language built for Swift.
.package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"), .package(url: "https://github.com/vapor/leaf.git", from: "4.3.0"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors // 🔵 Non-blocking, event-driven networking for Swift. Used for, custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
.package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"), .package(url: "https://github.com/pointfreeco/swift-dependencies.git", from: "1.6.3"),
.package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"), .package(url: "https://github.com/sliemeobn/elementary.git", from: "0.3.2"),

View File

@@ -32,3 +32,70 @@ extension Employee.Update: Content {}
extension DatabaseClient.Employees: TestDependencyKey { extension DatabaseClient.Employees: TestDependencyKey {
public static let testValue = Self() public static let testValue = Self()
} }
#if DEBUG
typealias EmployeeMockStorage = MockStorage<
Employee,
Employee.Create,
DatabaseClient.Employees.FetchRequest,
Void,
Employee.Update
>
private extension EmployeeMockStorage {
init(_ mocks: [Employee]) {
@Dependency(\.date.now) var now
@Dependency(\.uuid) var uuid
self.init(
mocks,
create: { employee in
Employee(
id: uuid(),
active: employee.active ?? true,
createdAt: now,
firstName: employee.firstName,
lastName: employee.lastName,
updatedAt: now
)
},
fetch: { request in
switch request {
case .all:
return { _ in true }
case .active:
return { $0.active == true }
case .inactive:
return { $0.active == false }
}
},
update: { employee, updates in
let model = Employee(
id: employee.id,
active: updates.active ?? employee.active,
createdAt: employee.createdAt,
firstName: updates.firstName ?? employee.firstName,
lastName: updates.lastName ?? employee.lastName,
updatedAt: now
)
employee = model
}
)
}
}
public extension DatabaseClient.Employees {
static func mock(_ mocks: [Employee] = []) -> Self {
let storage = EmployeeMockStorage(mocks)
return .init(
create: { try await storage.create($0) },
delete: { try await storage.delete($0) },
fetchAll: { try await storage.fetchAll($0) },
get: { try await storage.get($0) },
update: { try await storage.update($0, $1) }
)
}
}
#endif

View File

@@ -0,0 +1,143 @@
#if DEBUG
import Dependencies
import Vapor
actor MockStorage<
Model: Identifiable,
Create: Sendable,
Fetch: Sendable,
Get: Sendable,
Update: Sendable
> where Model: Sendable {
@Dependency(\.date.now) var now
@Dependency(\.uuid) var uuid
private var storage: [Model.ID: Model]
private let modelFromCreate: @Sendable (Create) -> Model
private let fetchToPredicate: @Sendable (Fetch) -> ((Model) -> Bool)
private let fetchExtras: @Sendable (Fetch, [Model]) async throws -> [Model]
private let applyUpdates: @Sendable (inout Model, Update, Get?) async throws -> Void
private let getExtras: @Sendable (Get?, Model) async throws -> Model
init(
_ mocks: [Model] = [],
create modelFromCreate: @Sendable @escaping (Create) -> Model,
fetch fetchToPredicate: @Sendable @escaping (Fetch) -> ((Model) -> Bool),
fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 },
get getExtras: @Sendable @escaping (Get?, Model) async throws -> Model,
update applyUpdates: @Sendable @escaping (inout Model, Update, Get?) async throws -> Void
) {
self.storage = mocks.reduce(into: [Model.ID: Model]()) { $0[$1.id] = $1 }
self.modelFromCreate = modelFromCreate
self.fetchToPredicate = fetchToPredicate
self.fetchExtras = fetchExtras
self.applyUpdates = applyUpdates
self.getExtras = getExtras
}
func count() async throws -> Int {
storage.count
}
func create(_ create: Create) async throws -> Model {
let model = modelFromCreate(create)
storage[model.id] = model
return model
}
func delete(_ id: Model.ID) async throws {
storage[id] = nil
}
func fetchAll(_ request: Fetch) async throws -> [Model] {
let predicate = fetchToPredicate(request)
return try await fetchExtras(request, Array(storage.values.filter { predicate($0) }))
}
func get(_ id: Model.ID, _ request: Get?) async throws -> Model? {
if let model = storage[id] {
return try await getExtras(request, model)
}
return nil
}
func update(_ id: Model.ID, _ updates: Update, _ get: Get?) async throws -> Model {
guard var model = storage[id] else {
throw Abort(.badRequest, reason: "Model not found.")
}
try await applyUpdates(&model, updates, get)
storage[id] = model
return model
}
}
extension MockStorage where Get == Void {
init(
_ mocks: [Model] = [],
create modelFromCreate: @Sendable @escaping (Create) -> Model,
fetch fetchToPredicate: @Sendable @escaping (Fetch) -> ((Model) -> Bool),
fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 },
update applyUpdates: @Sendable @escaping (inout Model, Update) async throws -> Void
) {
self.init(
mocks,
create: modelFromCreate,
fetch: fetchToPredicate,
fetchExtras: fetchExtras,
get: { _, model in model },
update: { model, updates, _ in try await applyUpdates(&model, updates) }
)
}
func get(_ id: Model.ID) async throws -> Model? {
storage[id]
}
func update(_ id: Model.ID, _ updates: Update) async throws -> Model {
try await update(id, updates, ())
}
}
extension MockStorage where Fetch == Void {
init(
_ mocks: [Model] = [],
create modelFromCreate: @Sendable @escaping (Create) -> Model,
fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 },
get getExtras: @Sendable @escaping (Get?, Model) async throws -> Model,
update applyUpdates: @Sendable @escaping (inout Model, Update, Get?) async throws -> Void
) {
self.init(
mocks,
create: modelFromCreate,
fetch: { _ in { _ in true } },
fetchExtras: fetchExtras,
get: getExtras,
update: applyUpdates
)
}
func fetchAll() async throws -> [Model] {
try await fetchAll(())
}
}
extension MockStorage where Fetch == Void, Get == Void {
init(
_ mocks: [Model] = [],
create modelFromCreate: @Sendable @escaping (Create) -> Model,
fetchExtras: @Sendable @escaping (Fetch, [Model]) async throws -> [Model] = { $1 },
update applyUpdates: @Sendable @escaping (inout Model, Update) async throws -> Void
) {
self.init(
mocks,
create: modelFromCreate,
fetchExtras: fetchExtras,
get: { _, model in model },
update: { model, updates, _ in try await applyUpdates(&model, updates) }
)
}
}
#endif

View File

@@ -15,7 +15,6 @@ public extension DatabaseClient {
public enum FetchRequest: Equatable { public enum FetchRequest: Equatable {
case all case all
case `for`(vendorID: Vendor.ID) case `for`(vendorID: Vendor.ID)
case withVendor
} }
public func fetchAll() async throws -> [VendorBranch] { public func fetchAll() async throws -> [VendorBranch] {
@@ -32,3 +31,61 @@ extension DatabaseClient.VendorBranches.FetchRequest: Content {}
extension DatabaseClient.VendorBranches: TestDependencyKey { extension DatabaseClient.VendorBranches: TestDependencyKey {
public static let testValue: DatabaseClient.VendorBranches = Self() public static let testValue: DatabaseClient.VendorBranches = Self()
} }
#if DEBUG
typealias VendorBranchMockStorage = MockStorage<
VendorBranch,
VendorBranch.Create,
DatabaseClient.VendorBranches.FetchRequest,
Void,
VendorBranch.Update
>
private extension VendorBranchMockStorage {
init(_ mocks: [VendorBranch]) {
@Dependency(\.date.now) var now
@Dependency(\.uuid) var uuid
self.init(
mocks,
create: {
VendorBranch(id: uuid(), name: $0.name, vendorID: $0.vendorID, createdAt: now, updatedAt: now)
},
fetch: { request in
switch request {
case .all:
return { _ in true }
case let .for(vendorID):
return { $0.vendorID == vendorID }
}
},
update: { branch, updates in
let model = VendorBranch(
id: branch.id,
name: updates.name ?? branch.name,
vendorID: branch.vendorID,
createdAt: branch.createdAt,
updatedAt: now
)
branch = model
}
)
}
}
public extension DatabaseClient.VendorBranches {
static func mock(_ mocks: [VendorBranch] = []) -> Self {
let storage = VendorBranchMockStorage(mocks)
return .init(
create: { try await storage.create($0) },
delete: { try await storage.delete($0) },
fetchAll: { try await storage.fetchAll($0) },
get: { try await storage.get($0) },
update: { try await storage.update($0, $1) }
)
}
}
#endif

View File

@@ -51,3 +51,94 @@ extension DatabaseClient.Vendors.GetRequest: Content {}
extension DatabaseClient.Vendors: TestDependencyKey { extension DatabaseClient.Vendors: TestDependencyKey {
public static let testValue: DatabaseClient.Vendors = Self() public static let testValue: DatabaseClient.Vendors = Self()
} }
#if DEBUG
typealias VendorMockStorage = MockStorage<
Vendor,
Vendor.Create,
DatabaseClient.Vendors.FetchRequest,
DatabaseClient.Vendors.GetRequest,
Vendor.Update
>
private extension VendorMockStorage {
// swiftlint:disable function_body_length
static func vendors(_ mocks: [Vendor]) -> Self {
@Dependency(\.date.now) var now
@Dependency(\.uuid) var uuid
@Dependency(\.database.vendorBranches) var vendorBranches
return .init(
mocks,
create: {
Vendor(
id: uuid(),
name: $0.name,
createdAt: now,
updatedAt: now
)
},
fetch: { _ in
{ _ in true }
},
fetchExtras: { request, models in
guard request == .withBranches else { return models }
let branches = try await vendorBranches.fetchAll()
return models.map { model in
Vendor(
id: model.id,
name: model.name,
branches: Array(branches.filter { $0.vendorID == model.id }),
createdAt: model.createdAt,
updatedAt: model.updatedAt
)
}
},
get: { req, model in
guard req == .withBranches else { return model }
let branches = try await vendorBranches.fetchAll(.for(vendorID: model.id))
return Vendor(
id: model.id,
name: model.name,
branches: branches,
createdAt: model.createdAt,
updatedAt: model.updatedAt
)
},
update: { model, updates, get in
var branches: [VendorBranch]?
if get == .withBranches {
branches = try await vendorBranches.fetchAll(.for(vendorID: model.id))
}
let vendor = Vendor(
id: model.id,
name: updates.name ?? model.name,
branches: branches ?? model.branches,
createdAt: model.createdAt,
updatedAt: now
)
model = vendor
}
)
}
// swiftlint:enable function_body_length
}
public extension DatabaseClient.Vendors {
static func mock(_ mocks: [Vendor]) -> Self {
let storage = VendorMockStorage.vendors(mocks)
return .init(
create: { try await storage.create($0) },
delete: { try await storage.delete($0) },
fetchAll: { try await storage.fetchAll($0) },
get: { try await storage.get($0, $1) },
update: { req, updates, get in try await storage.update(req, updates, get) }
)
}
}
#endif

View File

@@ -16,13 +16,11 @@ public extension DatabaseClient.VendorBranches {
} }
try await model.delete(on: db) try await model.delete(on: db)
} fetchAll: { request in } fetchAll: { request in
var query = VendorBranchModel.query(on: db) let query = VendorBranchModel.query(on: db)
switch request { switch request {
case .all: case .all:
break break
case .withVendor:
query = query.with(\.$vendor)
case let .for(vendorID: vendorID): case let .for(vendorID: vendorID):
let branches = try await VendorModel.query(on: db) let branches = try await VendorModel.query(on: db)
.filter(\.$id == vendorID) .filter(\.$id == vendorID)

View File

@@ -1,6 +1,7 @@
@testable import DatabaseClientLive @testable import DatabaseClientLive
import Dependencies import Dependencies
import FluentSQLiteDriver import FluentSQLiteDriver
import Foundation
import Logging import Logging
import NIO import NIO
import SharedModels import SharedModels
@@ -48,10 +49,12 @@ struct DatabaseClientTests {
} }
} }
@Test @Test(arguments: EmployeeTestFactory.testCases)
func employees() async throws { func employees(factory: EmployeeTestFactory) async throws {
try await withDatabase(migrations: Employee.Migrate()) { try await withDatabase(migrations: Employee.Migrate()) {
$0.database.employees = .live(database: $1) $0.uuid = .incrementing
$0.date = .init { Date() }
$0.database.employees = factory.handler($1)
} operation: { } operation: {
@Dependency(\.database.employees) var employees @Dependency(\.database.employees) var employees
@@ -90,11 +93,11 @@ struct DatabaseClientTests {
} }
} }
@Test @Test(arguments: VendorTestFactory.testCases)
func vendors() async throws { func vendors(factory: VendorTestFactory) async throws {
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) { try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
$0.database.vendors = .live(database: $1) $0.database.vendorBranches = factory.handler($1).0
$0.database.vendorBranches = .live(database: $1) $0.database.vendors = factory.handler($1).1
} operation: { } operation: {
@Dependency(\.database.vendorBranches) var branches @Dependency(\.database.vendorBranches) var branches
@Dependency(\.database.vendors) var vendors @Dependency(\.database.vendors) var vendors
@@ -119,7 +122,7 @@ struct DatabaseClientTests {
let fetchedWithBranches = try await vendors.get(vendor.id, .withBranches) let fetchedWithBranches = try await vendors.get(vendor.id, .withBranches)
#expect(fetchedWithBranches!.branches!.first == branch) #expect(fetchedWithBranches!.branches!.first == branch)
let updated = try await vendors.update(vendor.id, .init(name: "Johnstone")) let updated = try await vendors.update(vendor.id, with: .init(name: "Johnstone"))
#expect(updated.name == "Johnstone") #expect(updated.name == "Johnstone")
try await vendors.delete(vendor.id) try await vendors.delete(vendor.id)
@@ -129,11 +132,11 @@ struct DatabaseClientTests {
} }
} }
@Test @Test(arguments: VendorTestFactory.testCases)
func vendorBranches() async throws { func vendorBranches(factory: VendorTestFactory) async throws {
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) { try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
$0.database.vendors = .live(database: $1) $0.database.vendorBranches = factory.handler($1).0
$0.database.vendorBranches = .live(database: $1) $0.database.vendors = factory.handler($1).1
} operation: { } operation: {
@Dependency(\.database.vendorBranches) var branches @Dependency(\.database.vendorBranches) var branches
@Dependency(\.database.vendors) var vendors @Dependency(\.database.vendors) var vendors
@@ -180,6 +183,8 @@ struct DatabaseClientTests {
} }
try await withDependencies { try await withDependencies {
$0.uuid = .incrementing
$0.date = .init { Date() }
setupDependencies(&$0, database) setupDependencies(&$0, database)
} operation: { } operation: {
try await operation() try await operation()
@@ -192,3 +197,22 @@ struct DatabaseClientTests {
await dbs.shutdownAsync() await dbs.shutdownAsync()
} }
} }
struct EmployeeTestFactory {
let handler: (any Database) -> DatabaseClient.Employees
static var testCases: [Self] { [
.init(handler: { .live(database: $0) }),
.init(handler: { _ in .mock([]) })
] }
}
struct VendorTestFactory {
let handler: (any Database) -> (DatabaseClient.VendorBranches, DatabaseClient.Vendors)
static var testCases: [Self] { [
.init(handler: { (.live(database: $0), .live(database: $0)) }),
.init(handler: { _ in (.mock([]), .mock([])) })
] }
}