跳到主要内容

集成测试

集成测试侧重于测试程序的不同部分如何协同工作。在使用数据库的应用程序的上下文中,集成测试通常需要数据库可用,并包含方便测试预期场景的数据。

模拟真实环境的一种方法是使用 Docker 来封装数据库和一些测试数据。这可以随着测试一起启动和拆卸,从而作为一个与你的生产数据库隔离的环境运行。

注意: 这篇博客文章提供了关于设置集成测试环境和针对真实数据库编写集成测试的全面指南,为那些希望探索此主题的人提供了宝贵的见解。

先决条件

本指南假设你的机器上已安装 DockerDocker Compose,并且你的项目中已设置 Jest

本指南将使用以下电子商务 schema。这与文档其他部分中使用的传统 UserPost 模型有所不同,主要是因为你不太可能针对你的博客运行集成测试。

电子商务 schema
schema.prisma
// Can have 1 customer
// Can have many order details
model CustomerOrder {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
customer Customer @relation(fields: [customerId], references: [id])
customerId Int
orderDetails OrderDetails[]
}

// Can have 1 order
// Can have many products
model OrderDetails {
id Int @id @default(autoincrement())
products Product @relation(fields: [productId], references: [id])
productId Int
order CustomerOrder @relation(fields: [orderId], references: [id])
orderId Int
total Decimal
quantity Int
}

// Can have many order details
// Can have 1 category
model Product {
id Int @id @default(autoincrement())
name String
description String
price Decimal
sku Int
orderDetails OrderDetails[]
category Category @relation(fields: [categoryId], references: [id])
categoryId Int
}

// Can have many products
model Category {
id Int @id @default(autoincrement())
name String
products Product[]
}

// Can have many orders
model Customer {
id Int @id @default(autoincrement())
email String @unique
address String?
name String?
orders CustomerOrder[]
}

本指南对 Prisma Client 设置使用了单例模式。有关如何设置的详细步骤,请参阅单例文档。

将 Docker 添加到你的项目

Docker compose code pointing towards image of container holding a Postgres database

在你的机器上安装 Docker 和 Docker compose 后,你可以在你的项目中使用它们。

  1. 首先在你的项目根目录创建一个 docker-compose.yml 文件。在这里,你将添加一个 Postgres 镜像并指定环境凭据。
docker-compose.yml
# Set the version of docker compose to use
version: '3.9'

# The containers that compose the project
services:
db:
image: postgres:13
restart: always
container_name: integration-tests-prisma
ports:
- '5433:5432'
environment:
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
POSTGRES_DB: tests

注意:此处使用的 compose 版本 (3.9) 是编写时最新的版本,如果你正在按照指南操作,请务必使用相同的版本以保持一致性。

docker-compose.yml 文件定义了以下内容

  • Postgres 镜像 (postgres) 和版本标签 (:13)。如果你本地没有,这将自动下载。
  • 端口 5433 映射到内部(Postgres 默认)端口 5432。这将是数据库在外部公开的端口号。
  • 数据库用户凭据已设置,并且数据库已命名。
  1. 要连接到容器中的数据库,请使用在 docker-compose.yml 文件中定义的凭据创建一个新的连接字符串。例如
.env.test
DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"
信息

上面的 .env.test 文件用作多个 .env 文件设置的一部分。查看使用多个 .env 文件部分,了解有关使用多个 .env 文件设置项目的更多信息

  1. 要以分离状态创建容器,以便你可以继续使用终端选项卡,请运行以下命令
docker compose up -d
  1. 接下来,你可以通过在容器内执行 psql 命令来检查数据库是否已创建。记下容器 ID。

    docker ps
    显示CLI结果
    CONTAINER ID   IMAGE             COMMAND                  CREATED         STATUS        PORTS                    NAMES
    1322e42d833f postgres:13 "docker-entrypoint.s…" 2 seconds ago Up 1 second 0.0.0.0:5433->5432/tcp integration-tests-prisma

注意:容器 ID 对于每个容器都是唯一的,你将看到显示不同的 ID。

  1. 使用上一步中的容器 ID,在容器中运行 psql,使用创建的用户登录并检查数据库是否已创建

    docker exec -it 1322e42d833f psql -U prisma tests
    显示CLI结果
    tests=# \l
    List of databases
    Name | Owner | Encoding | Collate | Ctype | Access privileges

    postgres | prisma | UTF8 | en_US.utf8 | en_US.utf8 |
    template0 | prisma | UTF8 | en_US.utf8 | en_US.utf8 | =c/prisma +
    | | | | | prisma=CTc/prisma
    template1 | prisma | UTF8 | en_US.utf8 | en_US.utf8 | =c/prisma +
    | | | | | prisma=CTc/prisma
    tests | prisma | UTF8 | en_US.utf8 | en_US.utf8 |
    (4 rows)

集成测试

集成测试将在专用测试环境中的数据库上运行,而不是生产或开发环境。

操作流程

运行这些测试的流程如下

  1. 启动容器并创建数据库
  2. 迁移 schema
  3. 运行测试
  4. 销毁容器

每个测试套件将在运行所有测试之前播种数据库。在套件中的所有测试完成后,所有表中的数据将被删除,并且连接将终止。

要测试的函数

你正在测试的电子商务应用程序具有一个创建订单的函数。此函数执行以下操作

  • 接受关于下订单客户的输入
  • 接受关于订购产品的输入
  • 检查客户是否已存在帐户
  • 检查产品是否有库存
  • 如果产品不存在,则返回“缺货”消息
  • 如果客户在数据库中不存在,则创建帐户
  • 创建订单

下面可以看到这样一个函数可能的外观示例

create-order.ts
import prisma from '../client'

export interface Customer {
id?: number
name?: string
email: string
address?: string
}

export interface OrderInput {
customer: Customer
productId: number
quantity: number
}

/**
* Creates an order with customer.
* @param input The order parameters
*/
export async function createOrder(input: OrderInput) {
const { productId, quantity, customer } = input
const { name, email, address } = customer

// Get the product
const product = await prisma.product.findUnique({
where: {
id: productId,
},
})

// If the product is null its out of stock, return error.
if (!product) return new Error('Out of stock')

// If the customer is new then create the record, otherwise connect via their unique email
await prisma.customerOrder.create({
data: {
customer: {
connectOrCreate: {
create: {
name,
email,
address,
},
where: {
email,
},
},
},
orderDetails: {
create: {
total: product.price,
quantity,
products: {
connect: {
id: product.id,
},
},
},
},
},
})
}

测试套件

以下测试将检查 createOrder 函数是否按预期工作。它们将测试

  • 使用新客户创建新订单
  • 使用现有客户创建订单
  • 如果产品不存在,则显示“缺货”错误消息

在运行测试套件之前,数据库会预先填充数据。测试套件完成后,将使用 deleteMany 清除数据库中的数据。

提示

在使用你预先知道你的 schema 结构的情况下,使用 deleteMany 可能就足够了。这是因为操作需要根据模型关系的设置以正确的顺序执行。

但是,这不如拥有一个更通用的解决方案(该解决方案映射你的模型并对其执行 truncate 操作)那样可扩展。对于这些场景以及使用原始 SQL 查询的示例,请参阅使用原始 SQL / TRUNCATE 删除所有数据

__tests__/create-order.ts
import prisma from '../src/client'
import { createOrder, Customer, OrderInput } from '../src/functions/index'

beforeAll(async () => {
// create product categories
await prisma.category.createMany({
data: [{ name: 'Wand' }, { name: 'Broomstick' }],
})

console.log('✨ 2 categories successfully created!')

// create products
await prisma.product.createMany({
data: [
{
name: 'Holly, 11", phoenix feather',
description: 'Harry Potters wand',
price: 100,
sku: 1,
categoryId: 1,
},
{
name: 'Nimbus 2000',
description: 'Harry Potters broom',
price: 500,
sku: 2,
categoryId: 2,
},
],
})

console.log('✨ 2 products successfully created!')

// create the customer
await prisma.customer.create({
data: {
name: 'Harry Potter',
email: '[email protected]',
address: '4 Privet Drive',
},
})

console.log('✨ 1 customer successfully created!')
})

afterAll(async () => {
const deleteOrderDetails = prisma.orderDetails.deleteMany()
const deleteProduct = prisma.product.deleteMany()
const deleteCategory = prisma.category.deleteMany()
const deleteCustomerOrder = prisma.customerOrder.deleteMany()
const deleteCustomer = prisma.customer.deleteMany()

await prisma.$transaction([
deleteOrderDetails,
deleteProduct,
deleteCategory,
deleteCustomerOrder,
deleteCustomer,
])

await prisma.$disconnect()
})

it('should create 1 new customer with 1 order', async () => {
// The new customers details
const customer: Customer = {
id: 2,
name: 'Hermione Granger',
email: '[email protected]',
address: '2 Hampstead Heath',
}
// The new orders details
const order: OrderInput = {
customer,
productId: 1,
quantity: 1,
}

// Create the order and customer
await createOrder(order)

// Check if the new customer was created by filtering on unique email field
const newCustomer = await prisma.customer.findUnique({
where: {
email: customer.email,
},
})

// Check if the new order was created by filtering on unique email field of the customer
const newOrder = await prisma.customerOrder.findFirst({
where: {
customer: {
email: customer.email,
},
},
})

// Expect the new customer to have been created and match the input
expect(newCustomer).toEqual(customer)
// Expect the new order to have been created and contain the new customer
expect(newOrder).toHaveProperty('customerId', 2)
})

it('should create 1 order with an existing customer', async () => {
// The existing customers email
const customer: Customer = {
email: '[email protected]',
}
// The new orders details
const order: OrderInput = {
customer,
productId: 1,
quantity: 1,
}

// Create the order and connect the existing customer
await createOrder(order)

// Check if the new order was created by filtering on unique email field of the customer
const newOrder = await prisma.customerOrder.findFirst({
where: {
customer: {
email: customer.email,
},
},
})

// Expect the new order to have been created and contain the existing customer with an id of 1 (Harry Potter from the seed script)
expect(newOrder).toHaveProperty('customerId', 1)
})

it("should show 'Out of stock' message if productId doesn't exit", async () => {
// The existing customers email
const customer: Customer = {
email: '[email protected]',
}
// The new orders details
const order: OrderInput = {
customer,
productId: 3,
quantity: 1,
}

// The productId supplied doesn't exit so the function should return an "Out of stock" message
await expect(createOrder(order)).resolves.toEqual(new Error('Out of stock'))
})

运行测试

此设置隔离了一个真实世界的场景,以便你可以在受控环境中针对真实数据测试你的应用程序功能。

你可以向你的项目的 package.json 文件添加一些脚本,这些脚本将设置数据库并运行测试,然后在之后手动销毁容器。

警告

如果测试对你不起作用,你将需要确保测试数据库已正确设置并准备就绪,如这篇博客中所述。

package.json
  "scripts": {
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"test": "yarn docker:up && yarn prisma migrate deploy && jest -i"
},

test 脚本执行以下操作

  1. 运行 docker compose up -d 以使用 Postgres 镜像和数据库创建容器。
  2. ./prisma/migrations/ 目录中找到的迁移应用到数据库,这将在容器的数据库中创建表。
  3. 执行测试。

一旦你满意,你可以运行 yarn docker:down 来销毁容器、其数据库和任何测试数据。