feat: Adds basic tests for database client.

This commit is contained in:
2025-01-13 16:33:18 -05:00
parent 217dc5fa56
commit b38dc0d4e3
6 changed files with 313 additions and 109 deletions

1
.gitignore vendored
View File

@@ -10,3 +10,4 @@ db.sqlite
.env.*
! .env.example
.vscode
.nvim

View File

@@ -6,6 +6,11 @@ let package = Package(
platforms: [
.macOS(.v14)
],
products: [
.library(name: "SharedModels", targets: ["SharedModels"]),
.library(name: "DatabaseClient", targets: ["DatabaseClient"]),
.library(name: "DatabaseClientLive", targets: ["DatabaseClientLive"])
],
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.110.1"),
@@ -62,6 +67,13 @@ let package = Package(
],
swiftSettings: swiftSettings
),
.testTarget(
name: "DatabaseClientTests",
dependencies: [
"DatabaseClientLive",
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver")
]
),
.target(
name: "SharedModels",
dependencies: [

View File

@@ -9,7 +9,7 @@ public extension DatabaseClient.Employees {
.init { create in
let model = try create.toModel()
try await model.save(on: database)
return model.toDTO()
return try model.toDTO()
} delete: { id in
guard let model = try await EmployeeModel.find(id, on: database) else {
throw NotFoundError()
@@ -28,17 +28,17 @@ public extension DatabaseClient.Employees {
break
}
return try await query.all().map { $0.toDTO() }
return try await query.all().map { try $0.toDTO() }
} get: { id in
try await EmployeeModel.find(id, on: database).map { $0.toDTO() }
try await EmployeeModel.find(id, on: database).map { try $0.toDTO() }
} update: { id, updates in
guard let model = try await EmployeeModel.find(id, on: database) else {
throw NotFoundError()
}
try model.applyUpdate(updates)
try await model.save(on: database)
return model.toDTO()
return try model.toDTO()
}
}
}
@@ -150,14 +150,14 @@ final class EmployeeModel: Model, @unchecked Sendable {
self.updatedAt = updatedAt
}
func toDTO() -> Employee {
.init(
id: id,
func toDTO() throws -> Employee {
try .init(
id: requireID(),
active: active,
createdAt: createdAt,
createdAt: createdAt!,
firstName: firstName,
lastName: lastName,
updatedAt: updatedAt
updatedAt: updatedAt!
)
}

View File

@@ -10,32 +10,29 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable {
public var updatedAt: Date
public init(
id: UUID? = nil,
id: UUID,
active: Bool = true,
createdAt: Date? = nil,
createdAt: Date,
firstName: String,
lastName: String,
updatedAt: Date? = nil
updatedAt: Date
) {
@Dependency(\.date) var date
@Dependency(\.uuid) var uuid
self.id = id ?? uuid()
self.id = id
self.active = active
self.createdAt = createdAt ?? date.now
self.createdAt = createdAt
self.firstName = firstName
self.lastName = lastName
self.updatedAt = updatedAt ?? date.now
self.updatedAt = updatedAt
}
}
public extension Employee {
static var mocks: [Self] {
[
.init(firstName: "Michael", lastName: "Housh"),
.init(firstName: "Blob", lastName: "Esquire"),
.init(firstName: "Testy", lastName: "McTestface")
]
}
}
// public extension Employee {
// static var mocks: [Self] {
// [
// .init(firstName: "Michael", lastName: "Housh"),
// .init(firstName: "Blob", lastName: "Esquire"),
// .init(firstName: "Testy", lastName: "McTestface")
// ]
// }
// }

View File

@@ -1,82 +1,82 @@
@testable import App
import VaporTesting
import Testing
import Fluent
@Suite("App Tests with DB", .serialized)
struct AppTests {
private func withApp(_ test: (Application) async throws -> ()) async throws {
let app = try await Application.make(.testing)
do {
try await configure(app)
try await app.autoMigrate()
try await test(app)
try await app.autoRevert()
}
catch {
try? await app.autoRevert()
try await app.asyncShutdown()
throw error
}
try await app.asyncShutdown()
}
@Test("Test Hello World Route")
func helloWorld() async throws {
try await withApp { app in
try await app.testing().test(.GET, "hello", afterResponse: { res async in
#expect(res.status == .ok)
#expect(res.body.string == "Hello, world!")
})
}
}
@Test("Getting all the Todos")
func getAllTodos() async throws {
try await withApp { app in
let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
try await sampleTodos.create(on: app.db)
try await app.testing().test(.GET, "todos", afterResponse: { res async throws in
#expect(res.status == .ok)
#expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
})
}
}
@Test("Creating a Todo")
func createTodo() async throws {
let newDTO = TodoDTO(id: nil, title: "test")
try await withApp { app in
try await app.testing().test(.POST, "todos", beforeRequest: { req in
try req.content.encode(newDTO)
}, afterResponse: { res async throws in
#expect(res.status == .ok)
let models = try await Todo.query(on: app.db).all()
#expect(models.map({ $0.toDTO().title }) == [newDTO.title])
})
}
}
@Test("Deleting a Todo")
func deleteTodo() async throws {
let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
try await withApp { app in
try await testTodos.create(on: app.db)
try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
#expect(res.status == .noContent)
let model = try await Todo.find(testTodos[0].id, on: app.db)
#expect(model == nil)
})
}
}
}
extension TodoDTO: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title
}
}
// @testable import App
// import VaporTesting
// import Testing
// import Fluent
//
// @Suite("App Tests with DB", .serialized)
// struct AppTests {
// private func withApp(_ test: (Application) async throws -> ()) async throws {
// let app = try await Application.make(.testing)
// do {
// try await configure(app)
// try await app.autoMigrate()
// try await test(app)
// try await app.autoRevert()
// }
// catch {
// try? await app.autoRevert()
// try await app.asyncShutdown()
// throw error
// }
// try await app.asyncShutdown()
// }
//
// @Test("Test Hello World Route")
// func helloWorld() async throws {
// try await withApp { app in
// try await app.testing().test(.GET, "hello", afterResponse: { res async in
// #expect(res.status == .ok)
// #expect(res.body.string == "Hello, world!")
// })
// }
// }
//
// @Test("Getting all the Todos")
// func getAllTodos() async throws {
// try await withApp { app in
// let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
// try await sampleTodos.create(on: app.db)
//
// try await app.testing().test(.GET, "todos", afterResponse: { res async throws in
// #expect(res.status == .ok)
// #expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
// })
// }
// }
//
// @Test("Creating a Todo")
// func createTodo() async throws {
// let newDTO = TodoDTO(id: nil, title: "test")
//
// try await withApp { app in
// try await app.testing().test(.POST, "todos", beforeRequest: { req in
// try req.content.encode(newDTO)
// }, afterResponse: { res async throws in
// #expect(res.status == .ok)
// let models = try await Todo.query(on: app.db).all()
// #expect(models.map({ $0.toDTO().title }) == [newDTO.title])
// })
// }
// }
//
// @Test("Deleting a Todo")
// func deleteTodo() async throws {
// let testTodos = [Todo(title: "test1"), Todo(title: "test2")]
//
// try await withApp { app in
// try await testTodos.create(on: app.db)
//
// try await app.testing().test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
// #expect(res.status == .noContent)
// let model = try await Todo.find(testTodos[0].id, on: app.db)
// #expect(model == nil)
// })
// }
// }
// }
//
// extension TodoDTO: Equatable {
// public static func == (lhs: Self, rhs: Self) -> Bool {
// lhs.id == rhs.id && lhs.title == rhs.title
// }
// }

View File

@@ -0,0 +1,194 @@
@testable import DatabaseClientLive
import Dependencies
import FluentSQLiteDriver
import Logging
import NIO
import SharedModels
import Testing
@Suite("DatabaseClientTests")
struct DatabaseClientTests {
let logger: Logger
init() async throws {
let logger = Logger(label: "database-client-tests")
self.logger = logger
}
@Test
func users() async throws {
try await withDatabase(migrations: User.Migrate()) {
$0.database.users = .live(database: $1)
} operation: {
@Dependency(\.database.users) var users
let user = try await users.create(.init(
username: "blob",
email: "blob@example.com",
password: "super-secret",
confirmPassword: "super-secret"
))
#expect(user.username == "blob")
#expect(user.email == "blob@example.com")
#expect(user.createdAt != nil)
#expect(user.updatedAt != nil)
let allUsers = try await users.fetchAll()
#expect(allUsers.count == 1)
#expect(allUsers.first == user)
let fetched = try await users.get(user.id)
#expect(fetched != nil)
#expect(fetched == user)
try await users.delete(user.id)
let allUsers2 = try await users.fetchAll()
#expect(allUsers2.count == 0)
}
}
@Test
func employees() async throws {
try await withDatabase(migrations: Employee.Migrate()) {
$0.database.employees = .live(database: $1)
} operation: {
@Dependency(\.database.employees) var employees
let employee = try await employees.create(.init(
firstName: "Blob",
lastName: "Esquire"
))
#expect(employee.firstName == "Blob")
#expect(employee.lastName == "Esquire")
#expect(employee.active)
let allEmployees = try await employees.fetchAll()
#expect(allEmployees.count == 1)
#expect(allEmployees.first == employee)
let inActiveEmployees = try await employees.fetchAll(.inactive)
#expect(inActiveEmployees.count == 0)
let activeEmployees = try await employees.fetchAll(.active)
#expect(activeEmployees == allEmployees)
let fetched = try await employees.get(employee.id)
#expect(fetched == employee)
let updated = try await employees.update(employee.id, Employee.Update(active: false))
#expect(updated.active == false)
let inActiveEmployees2 = try await employees.fetchAll(.inactive)
#expect(inActiveEmployees2.count == 1)
try await employees.delete(employee.id)
let shouldBeNone = try await employees.fetchAll()
#expect(shouldBeNone.count == 0)
}
}
@Test
func vendors() async throws {
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
$0.database.vendors = .live(database: $1)
$0.database.vendorBranches = .live(database: $1)
} operation: {
@Dependency(\.database.vendorBranches) var branches
@Dependency(\.database.vendors) var vendors
let vendor = try await vendors.create(.init(name: "Corken"))
#expect(vendor.name == "Corken")
let branch = try await branches.create(.init(name: "Monroe", vendorID: vendor.id))
#expect(branch.name == "Monroe")
let all = try await vendors.fetchAll()
#expect(all.count == 1)
#expect(all.first == vendor)
let allWithBranches = try await vendors.fetchAll(.withBranches)
#expect(allWithBranches.count == 1)
#expect(allWithBranches.first!.branches!.first == branch)
let fetched = try await vendors.get(vendor.id)
#expect(fetched == vendor)
let fetchedWithBranches = try await vendors.get(vendor.id, .withBranches)
#expect(fetchedWithBranches!.branches!.first == branch)
let updated = try await vendors.update(vendor.id, .init(name: "Johnstone"))
#expect(updated.name == "Johnstone")
try await vendors.delete(vendor.id)
let shouldBeNone = try await vendors.fetchAll()
#expect(shouldBeNone.count == 0)
}
}
@Test
func vendorBranches() async throws {
try await withDatabase(migrations: Vendor.Migrate(), VendorBranch.Migrate()) {
$0.database.vendors = .live(database: $1)
$0.database.vendorBranches = .live(database: $1)
} operation: {
@Dependency(\.database.vendorBranches) var branches
@Dependency(\.database.vendors) var vendors
let vendor = try await vendors.create(.init(name: "Corken"))
#expect(vendor.name == "Corken")
let branch = try await branches.create(.init(name: "Monroe", vendorID: vendor.id))
#expect(branch.name == "Monroe")
let all = try await branches.fetchAll()
#expect(all.count == 1)
let fetched = try await branches.get(branch.id)
#expect(fetched == branch)
let updated = try await branches.update(branch.id, .init(name: "Covington"))
#expect(updated.name == "Covington")
try await branches.delete(branch.id)
let shouldBeNone = try await branches.fetchAll()
#expect(shouldBeNone.count == 0)
}
}
// Helper to create an in-memory database for testing.
func withDatabase(
migrations: (any AsyncMigration)...,
setupDependencies: (inout DependencyValues, any Database) -> Void,
operation: () async throws -> Void
) async throws {
let dbs = Databases(threadPool: NIOThreadPool.singleton, on: MultiThreadedEventLoopGroup.singleton)
dbs.use(.sqlite(.memory), as: .sqlite)
let database = dbs.database(
.sqlite,
logger: .init(label: "test.sqlite"),
on: MultiThreadedEventLoopGroup.singleton.any()
)!
for migration in migrations {
try await migration.prepare(on: database)
}
try await withDependencies {
setupDependencies(&$0, database)
} operation: {
try await operation()
}
for migration in migrations {
try await migration.revert(on: database)
}
await dbs.shutdownAsync()
}
}