feat: Adds basic tests for database client.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,3 +10,4 @@ db.sqlite
|
||||
.env.*
|
||||
! .env.example
|
||||
.vscode
|
||||
.nvim
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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!
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
// }
|
||||
|
||||
194
Tests/DatabaseClientTests/DatabaseClientTests.swift
Normal file
194
Tests/DatabaseClientTests/DatabaseClientTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user