在本章,我们将设计 Micro Blog 中 Post(帖子)的数据模型,并使用 PostgreSQL 作为我们的数据库来存储内容,在最后,我们会编写一个单元测试,来测试 Post 的创建功能。
.. toc:: Table of Contents :max-level: 3
在我们平时使用的微博系统里,帖子都是由用户发布的,因此作为数据模型的设计,通常的顺序也是先设计用户,然后再设计 Post 的数据模型。
但为了更好的理解如何构建数据之间的关系,我决定这次从 Post 的数据模型开始。
一个简单的 Post 含有以下两个属性
如果用表格来表示我们的数据,那么看起来就是这样的
| content | createdAt |
|---|---|
| 这是第一篇博文 | 2023/7/9 14:42 |
当发布了更多的数据的时候,表格内容就会变成这样
| content | createdAt |
|---|---|
| 这是第一篇博文 | 2023/7/9 14:42 |
| 在 LONCAFE 写代码感觉不错呢! | 2023/7/9 14:44 |
我们最好给每条记录再加上一个不重复的 ID,这样我们就可以通过 ID 来快速准确的表示某一条帖子。
| id | content | createdAt |
|---|---|---|
| 0 | 这是第一篇博文 | 2023/7/9 14:42 |
| 1 | 在 LONCAFE 写代码感觉不错呢! | 2023/7/9 14:44 |
Hint
事实上数据在存储在数据库的时候,也正是一个个类似这样的表格
接下来,我们尝试用 Swift 中的 类 来表示这个数据结构
class Post {
let id: Int
let content: String
let createdAt: Date
}这样,我们就得到了 Post 这个数据模型最原始的状态。
现在 Vapor 还不知道如何在数据库里操作 Post 类型的数据,因为我们还有很多信息没有提供给 Vapor,比如:
那么接下来就一步步的解决这些问题
Vapor 使用自己的 Fluent 来完成与数据库的通信,这个功能也被通称为 ORM (Object-relational mapping)
我们修改 Package.swift 来加入 Fluent 的依赖,因为我们不再是一个简单的 HelloWorld 了,因此也顺便把 name 改成 MicroBlog 吧。
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
]
),
]
)接下来,我们创建 Sources/App/Models/Post.swift
import Vapor
import Fluent
final class Post: Model {
// 数据库中的表名
static let schema = "posts"
// 唯一性标识符
@ID(key: .id)
var id: UUID?
// 内容
@Field(key: "content")
var content: String
// 创建时间
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() { }
init(id: UUID? = nil, content: String) {
self.id = id
self.content = content
}
}在 Vapor 中,我们通过一些 Property Wrapper 来辅助完成 Model 和数据库中表的关系映射
@ID 是数据在数据库表中的「唯一性标识符」,在 Vapor 中默认推荐使用 UUID 随机字符串来作为 ID,并会默认使用字符串 id 在数据库的表中作为字段名,你可以使用 @ID(custom: "") 来修改这个字段名。@Field 是表示要存储在数据库中的数据属性,通过 @Field(key: "content") 我们显式的声明了 var content: String 对应的是数据库表中 content 这个字段。@Timestamp 是一个特殊的 @Field 类型,专门用来表示存储的是时间,同时带有一个 trigger 功能,在这里我们使用 on: .create 来表示,当 Post 被创建时,自动记录时间。至此 Vapor 就可以认识 Post 这个数据类型啦。
我们使用 PostgreSQL 作为我们的数据库服务,直接在电脑上安装 PostgreSQL 是一件比较复杂的事情,通过 Docker 我们可以简化这个过程。
Hint
Docker 是一种轻量级的容器化技术,将 App 运行所需要的运行时封装在一起变成一个沙盒环境,通过这项技术,我们可以在 Linux 系统上无缝的启动其他 App 而不需要在 Host 上安装各种依赖。你可以通过安装 Docker Desktop 来使用这项技术
docker-compos.yml 是容器编排文件,我们在这个文件中描述自己所需要的容器 App 以及其运行的环境变量,网络设置等。
首先在项目根目录里创建 docker-compose.yml ,编写以下内容
version: '3.7' # 定义 Docker Compose 文件的版本,此处使用的是版本 3.7
volumes: # 定义卷部分
db_data: # docker 会使用这个键作为名字,自动创建 db_data 卷来存储数据
services: # 定义服务部分
db: # db 服务配置
image: 'postgres:15-alpine' # 使用 PostgreSQL 15 Alpine 版本的镜像
volumes: # 定义挂载卷
- 'db_data:/var/lib/postgresql/data/pgdata' # 将 db_data 卷挂载到容器的 /var/lib/postgresql/data/pgdata 目录
environment: # 定义环境变量
PGDATA: '/var/lib/postgresql/data/pgdata' # 设置 PGDATA 环境变量为 /var/lib/postgresql/data/pgdata
POSTGRES_USER: 'vapor_username' # 设置 POSTGRES_USER 环境变量为 vapor_username
POSTGRES_PASSWORD: 'vapor_password' # 设置 POSTGRES_PASSWORD 环境变量为 vapor_password
POSTGRES_DB: 'vapor_database' # 设置 POSTGRES_DB 环境变量为 vapor_database
ports: # 定义端口映射,将主机的 5432 端口映射到容器的 5432 端口
- '5432:5432'现在我们在终端中进入 docker-compose.yml 所在的位置,使用 docker-compose up db 命令就可以启动数据库服务了,数据库将在本机的 5432 端口监听。
Caution
如果你看到了这样的错误 Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 请检查是否启动了 Docker Desktop
接下来,我们的目标是让 Vapor 获取到我们的服务器信息,连接到我们的服务器。
我们首先修改 Package.swift 增加 Fluent 对 PostgreSQL 的支持
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
]
),
]
)随后,在 Sources/App/main.swift 中增加连接数据库的信息
// ...
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: "localhost",
port: 5432,
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
try app.run()至此,Vapor 便知道如何连接到数据库了,但目前数据库还是一张白纸,并没有建立我们所需要的数据表,因此,在最后我们还需要写一个叫做 Migration 的东西,来更新数据库上的表结构。
Migration 是 Fluent 中用来对数据库表结构进行迁移的功能,接下来我们来了解如何使用这个工具。
在 Sources/App/Migrations/1_CreatePost.swift 写入以下内容
import Fluent
// 定义 CreatePost 结构体,实现 AsyncMigration 协议
struct CreatePost: AsyncMigration {
// 准备方法,在数据库上进行准备操作
func prepare(on database: Database) async throws {
// 创建 Post 表的数据库模式对象
try await database.schema(Post.schema)
.id() // 添加 id 列
.field("content", .string, .required) // 添加 content 列,类型为字符串,不能为空
.field("created_at", .datetime) // 添加 created_at 列,类型为日期时间
.create() // 创建 Post 表
}
// 回滚方法,在数据库上进行回滚操作
func revert(on database: Database) async throws {
try await database.schema(Post.schema).delete() // 删除 Post 表的数据库模式对象
}
}在运行时,以上的代码会被转换成 SQL 语句,以 prepare 中的代码为例,将会被转换成如下 SQL 代码
CREATE TABLE IF NOT EXISTS public.posts
(
id uuid NOT NULL,
content text COLLATE pg_catalog."default" NOT NULL,
created_at timestamp with time zone,
CONSTRAINT posts_pkey PRIMARY KEY (id)
)因此 Migration 也只是 Fluent 这个 ORM 对 SQL 的封装,通过这种封装,可以大幅减少我们编写 SQL 时出错的情况,并能通过常用场景的 SQL 语句优化来提升性能的表现。
如果你需要用到数据库的高级功能的话,Fluent 也支持直接使用 SQL 语句进行 Migration.
我们需要在 Sources/App/main.swit 中将 CreatePost 注册到 App 中以便一会我们执行 Migration 的时候,App 知道内容是什么
//...
app.migrations.add([CreatePost()])
try app.run()现在,在项目根目录执行 swift run App migrate ,输入 y 就可以完成数据库中表结构的更新。
The following migration(s) will be prepared:
+ App.CreatePost on default
Would you like to continue?
y/n> y接下来,我们编写 Unit Test 来测试 Post 的创建功能,在 Tests/AppTests/PostTests.swift 中写入以下内容
Hint
编写 Unit Test 可以针对功能进行自动化测试,确保我们服务器的功能不出现异常,我们将在后续章节中继续深入讨论 Unit Test 的使用
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
func testCreatePost() async throws {
let app = Application(.testing)
defer { app.shutdown() }
// autoRevert 将自动执行所有 Migration 中 revert 的内容
try await app.autoRevert()
// autoMigrate 将自动执行所有 Migration 中 prepare 的内容
// 这两步将重建我们的数据库,为我们提供一个干净的测试环境
try await app.autoMigrate()
let post = Post(content: "Hello, world!")
try await post.save(on: app.db)
let postID = try? post.requireID()
// 如果 postID 不为 nil 则成功创建,测试通过
XCTAssertNotNil(postID)
}
}随后,修改我们的 Package.swift 文件添加关于 Test 相关的描述
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
]
),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)现在,我们可以通过在 Package.swift 所在的路径执行 swift test 来运行单元测试
现在我们会获得一个测试没有通过的提示
FluentKit/Databases.swift:162: Fatal error: No default database configured.
error: Exited with signal code 5这是因为写在 main.swift 中关于数据库连接的内容并不会在测试中执行,我们需要重构这部分代码,使得两边都可以使用
在 Sources/App/configure.swift 中写入以下代码
import Fluent
import FluentPostgresDriver
import Vapor
public func configure(_ app: Application) async throws {
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: "localhost",
port: 5432,
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add([CreatePost()])
}修改 main.swift 使用 configure
import Vapor
import Fluent
import FluentPostgresDriver
let app = Application()
app.http.server.configuration.port = 8080
defer { app.shutdown() }
app.get { req async in
"It works!"
}
try await configure(app)
try app.run()修改 PostTests.swift 使用 configure
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
func testCreatePost() async throws {
let app = Application(.testing)
defer { app.shutdown() }
try await configure(app)
// autoRevert 将自动执行所有 Migration 中 revert 的内容
try await app.autoRevert()
// autoMigrate 将自动执行所有 Migration 中 prepare 的内容
// 这两步将重建我们的数据库,为我们提供一个干净的测试环境
try await app.autoMigrate()
let post = Post(content: "Hello, world!")
try await post.save(on: app.db)
let postID = try? post.requireID()
// 如果 postID 不为 nil 则成功创建,测试通过
XCTAssertNotNil(postID)
}
}现在运行 swift test 我们将会看到测试通过的信息
Test Case '-[AppTests.PostTests testCreatePost]' passed (0.263 seconds).
恭喜你,Vapor 和数据库连通起来了!
你可以在 https://github.com/kevinzhow/swift-on-server-tour/tree/main/2 找到本章的相关代码。
如果你希望查看数据库里创建了什么内容,使用 pgAdmin 可以连接到 PostgreSQL

在下一个章节,我们将编写 API 来实现 Post 的 CURD(Create Update Read Delete)并进一步学习测试的使用
在本章,我们将设计 Micro Blog 中 Post(帖子)的数据模型,并使用 PostgreSQL 作为我们的数据库来存储内容,在最后,我们会编写一个单元测试,来测试 Post 的创建功能。
.. toc:: Table of Contents :max-level: 3
在我们平时使用的微博系统里,帖子都是由用户发布的,因此作为数据模型的设计,通常的顺序也是先设计用户,然后再设计 Post 的数据模型。
但为了更好的理解如何构建数据之间的关系,我决定这次从 Post 的数据模型开始。
一个简单的 Post 含有以下两个属性
如果用表格来表示我们的数据,那么看起来就是这样的
| content | createdAt |
|---|---|
| 这是第一篇博文 | 2023/7/9 14:42 |
当发布了更多的数据的时候,表格内容就会变成这样
| content | createdAt |
|---|---|
| 这是第一篇博文 | 2023/7/9 14:42 |
| 在 LONCAFE 写代码感觉不错呢! | 2023/7/9 14:44 |
我们最好给每条记录再加上一个不重复的 ID,这样我们就可以通过 ID 来快速准确的表示某一条帖子。
| id | content | createdAt |
|---|---|---|
| 0 | 这是第一篇博文 | 2023/7/9 14:42 |
| 1 | 在 LONCAFE 写代码感觉不错呢! | 2023/7/9 14:44 |
Hint
事实上数据在存储在数据库的时候,也正是一个个类似这样的表格
接下来,我们尝试用 Swift 中的 类 来表示这个数据结构
class Post {
let id: Int
let content: String
let createdAt: Date
}这样,我们就得到了 Post 这个数据模型最原始的状态。
现在 Vapor 还不知道如何在数据库里操作 Post 类型的数据,因为我们还有很多信息没有提供给 Vapor,比如:
那么接下来就一步步的解决这些问题
Vapor 使用自己的 Fluent 来完成与数据库的通信,这个功能也被通称为 ORM (Object-relational mapping)
我们修改 Package.swift 来加入 Fluent 的依赖,因为我们不再是一个简单的 HelloWorld 了,因此也顺便把 name 改成 MicroBlog 吧。
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
]
),
]
)接下来,我们创建 Sources/App/Models/Post.swift
import Vapor
import Fluent
final class Post: Model {
// 数据库中的表名
static let schema = "posts"
// 唯一性标识符
@ID(key: .id)
var id: UUID?
// 内容
@Field(key: "content")
var content: String
// 创建时间
@Timestamp(key: "created_at", on: .create)
var createdAt: Date?
init() { }
init(id: UUID? = nil, content: String) {
self.id = id
self.content = content
}
}在 Vapor 中,我们通过一些 Property Wrapper 来辅助完成 Model 和数据库中表的关系映射
@ID 是数据在数据库表中的「唯一性标识符」,在 Vapor 中默认推荐使用 UUID 随机字符串来作为 ID,并会默认使用字符串 id 在数据库的表中作为字段名,你可以使用 @ID(custom: "") 来修改这个字段名。@Field 是表示要存储在数据库中的数据属性,通过 @Field(key: "content") 我们显式的声明了 var content: String 对应的是数据库表中 content 这个字段。@Timestamp 是一个特殊的 @Field 类型,专门用来表示存储的是时间,同时带有一个 trigger 功能,在这里我们使用 on: .create 来表示,当 Post 被创建时,自动记录时间。至此 Vapor 就可以认识 Post 这个数据类型啦。
我们使用 PostgreSQL 作为我们的数据库服务,直接在电脑上安装 PostgreSQL 是一件比较复杂的事情,通过 Docker 我们可以简化这个过程。
Hint
Docker 是一种轻量级的容器化技术,将 App 运行所需要的运行时封装在一起变成一个沙盒环境,通过这项技术,我们可以在 Linux 系统上无缝的启动其他 App 而不需要在 Host 上安装各种依赖。你可以通过安装 Docker Desktop 来使用这项技术
docker-compos.yml 是容器编排文件,我们在这个文件中描述自己所需要的容器 App 以及其运行的环境变量,网络设置等。
首先在项目根目录里创建 docker-compose.yml ,编写以下内容
version: '3.7' # 定义 Docker Compose 文件的版本,此处使用的是版本 3.7
volumes: # 定义卷部分
db_data: # docker 会使用这个键作为名字,自动创建 db_data 卷来存储数据
services: # 定义服务部分
db: # db 服务配置
image: 'postgres:15-alpine' # 使用 PostgreSQL 15 Alpine 版本的镜像
volumes: # 定义挂载卷
- 'db_data:/var/lib/postgresql/data/pgdata' # 将 db_data 卷挂载到容器的 /var/lib/postgresql/data/pgdata 目录
environment: # 定义环境变量
PGDATA: '/var/lib/postgresql/data/pgdata' # 设置 PGDATA 环境变量为 /var/lib/postgresql/data/pgdata
POSTGRES_USER: 'vapor_username' # 设置 POSTGRES_USER 环境变量为 vapor_username
POSTGRES_PASSWORD: 'vapor_password' # 设置 POSTGRES_PASSWORD 环境变量为 vapor_password
POSTGRES_DB: 'vapor_database' # 设置 POSTGRES_DB 环境变量为 vapor_database
ports: # 定义端口映射,将主机的 5432 端口映射到容器的 5432 端口
- '5432:5432'现在我们在终端中进入 docker-compose.yml 所在的位置,使用 docker-compose up db 命令就可以启动数据库服务了,数据库将在本机的 5432 端口监听。
Caution
如果你看到了这样的错误 Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 请检查是否启动了 Docker Desktop
接下来,我们的目标是让 Vapor 获取到我们的服务器信息,连接到我们的服务器。
我们首先修改 Package.swift 增加 Fluent 对 PostgreSQL 的支持
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
]
),
]
)随后,在 Sources/App/main.swift 中增加连接数据库的信息
// ...
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: "localhost",
port: 5432,
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
try app.run()至此,Vapor 便知道如何连接到数据库了,但目前数据库还是一张白纸,并没有建立我们所需要的数据表,因此,在最后我们还需要写一个叫做 Migration 的东西,来更新数据库上的表结构。
Migration 是 Fluent 中用来对数据库表结构进行迁移的功能,接下来我们来了解如何使用这个工具。
在 Sources/App/Migrations/1_CreatePost.swift 写入以下内容
import Fluent
// 定义 CreatePost 结构体,实现 AsyncMigration 协议
struct CreatePost: AsyncMigration {
// 准备方法,在数据库上进行准备操作
func prepare(on database: Database) async throws {
// 创建 Post 表的数据库模式对象
try await database.schema(Post.schema)
.id() // 添加 id 列
.field("content", .string, .required) // 添加 content 列,类型为字符串,不能为空
.field("created_at", .datetime) // 添加 created_at 列,类型为日期时间
.create() // 创建 Post 表
}
// 回滚方法,在数据库上进行回滚操作
func revert(on database: Database) async throws {
try await database.schema(Post.schema).delete() // 删除 Post 表的数据库模式对象
}
}在运行时,以上的代码会被转换成 SQL 语句,以 prepare 中的代码为例,将会被转换成如下 SQL 代码
CREATE TABLE IF NOT EXISTS public.posts
(
id uuid NOT NULL,
content text COLLATE pg_catalog."default" NOT NULL,
created_at timestamp with time zone,
CONSTRAINT posts_pkey PRIMARY KEY (id)
)因此 Migration 也只是 Fluent 这个 ORM 对 SQL 的封装,通过这种封装,可以大幅减少我们编写 SQL 时出错的情况,并能通过常用场景的 SQL 语句优化来提升性能的表现。
如果你需要用到数据库的高级功能的话,Fluent 也支持直接使用 SQL 语句进行 Migration.
我们需要在 Sources/App/main.swit 中将 CreatePost 注册到 App 中以便一会我们执行 Migration 的时候,App 知道内容是什么
//...
app.migrations.add([CreatePost()])
try app.run()现在,在项目根目录执行 swift run App migrate ,输入 y 就可以完成数据库中表结构的更新。
The following migration(s) will be prepared:
+ App.CreatePost on default
Would you like to continue?
y/n> y接下来,我们编写 Unit Test 来测试 Post 的创建功能,在 Tests/AppTests/PostTests.swift 中写入以下内容
Hint
编写 Unit Test 可以针对功能进行自动化测试,确保我们服务器的功能不出现异常,我们将在后续章节中继续深入讨论 Unit Test 的使用
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
func testCreatePost() async throws {
let app = Application(.testing)
defer { app.shutdown() }
// autoRevert 将自动执行所有 Migration 中 revert 的内容
try await app.autoRevert()
// autoMigrate 将自动执行所有 Migration 中 prepare 的内容
// 这两步将重建我们的数据库,为我们提供一个干净的测试环境
try await app.autoMigrate()
let post = Post(content: "Hello, world!")
try await post.save(on: app.db)
let postID = try? post.requireID()
// 如果 postID 不为 nil 则成功创建,测试通过
XCTAssertNotNil(postID)
}
}随后,修改我们的 Package.swift 文件添加关于 Test 相关的描述
// swift-tools-version:5.8
import PackageDescription
let package = Package(
name: "MicroBlog",
platforms: [
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.77.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.4.0"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
],
targets: [
.executableTarget(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor"),
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"),
]
),
.testTarget(name: "AppTests", dependencies: [
.target(name: "App"),
.product(name: "XCTVapor", package: "vapor"),
])
]
)现在,我们可以通过在 Package.swift 所在的路径执行 swift test 来运行单元测试
现在我们会获得一个测试没有通过的提示
FluentKit/Databases.swift:162: Fatal error: No default database configured.
error: Exited with signal code 5这是因为写在 main.swift 中关于数据库连接的内容并不会在测试中执行,我们需要重构这部分代码,使得两边都可以使用
在 Sources/App/configure.swift 中写入以下代码
import Fluent
import FluentPostgresDriver
import Vapor
public func configure(_ app: Application) async throws {
app.databases.use(.postgres(configuration: SQLPostgresConfiguration(
hostname: "localhost",
port: 5432,
username: "vapor_username",
password: "vapor_password",
database: "vapor_database",
tls: .prefer(try .init(configuration: .clientDefault)))
), as: .psql)
app.migrations.add([CreatePost()])
}修改 main.swift 使用 configure
import Vapor
import Fluent
import FluentPostgresDriver
let app = Application()
app.http.server.configuration.port = 8080
defer { app.shutdown() }
app.get { req async in
"It works!"
}
try await configure(app)
try app.run()修改 PostTests.swift 使用 configure
@testable import App
import XCTVapor
final class PostTests: XCTestCase {
func testCreatePost() async throws {
let app = Application(.testing)
defer { app.shutdown() }
try await configure(app)
// autoRevert 将自动执行所有 Migration 中 revert 的内容
try await app.autoRevert()
// autoMigrate 将自动执行所有 Migration 中 prepare 的内容
// 这两步将重建我们的数据库,为我们提供一个干净的测试环境
try await app.autoMigrate()
let post = Post(content: "Hello, world!")
try await post.save(on: app.db)
let postID = try? post.requireID()
// 如果 postID 不为 nil 则成功创建,测试通过
XCTAssertNotNil(postID)
}
}现在运行 swift test 我们将会看到测试通过的信息
Test Case '-[AppTests.PostTests testCreatePost]' passed (0.263 seconds).
恭喜你,Vapor 和数据库连通起来了!
你可以在 https://github.com/kevinzhow/swift-on-server-tour/tree/main/2 找到本章的相关代码。
如果你希望查看数据库里创建了什么内容,使用 pgAdmin 可以连接到 PostgreSQL

在下一个章节,我们将编写 API 来实现 Post 的 CURD(Create Update Read Delete)并进一步学习测试的使用