跳到主要內容

整合測試

整合測試著重於測試程式的不同部分如何協同工作。在使用資料庫的應用程式情境中,整合測試通常需要資料庫可用,並包含適用於預期測試情境的資料。

模擬真實世界環境的一種方法是使用 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 設定使用 singleton 模式。有關如何設定的逐步說明,請參閱 singleton 文件。

將 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: 'harry@hogwarts.io',
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: 'hermione@hogwarts.io',
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: 'harry@hogwarts.io',
}
// 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: 'harry@hogwarts.io',
}
// 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 來銷毀容器、其資料庫和任何測試資料。