統合テスト
統合テストは、プログラムの個別の部分がどのように連携するかをテストすることに焦点を当てています。データベースを使用するアプリケーションのコンテキストでは、統合テストは通常、データベースが利用可能であり、テストするシナリオに都合の良いデータが含まれている必要があります。
実環境をシミュレートする1つの方法は、Dockerを使用してデータベースといくつかのテストデータをカプセル化することです。これはテストと共に起動・停止できるため、本番データベースから隔離された環境として機能します。
注:このブログ記事は、統合テスト環境のセットアップと、実際のデータベースに対する統合テストの記述に関する包括的なガイドを提供しており、このトピックを深掘りしたい方にとって貴重な洞察を与えてくれます。
前提条件
このガイドは、お使いのコンピューターにDockerとDocker Composeがインストールされており、プロジェクトにJest
がセットアップされていることを前提としています。
このガイド全体で、以下のeコマーススキーマが使用されます。これは、ドキュメントの他の部分で使用されている従来のUser
およびPost
モデルとは異なりますが、主な理由は、ブログに対して統合テストを実行することはまずないためです。
eコマーススキーマ
// 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とDocker Composeが両方ともコンピューターにインストールされていれば、プロジェクトで使用できます。
- まず、プロジェクトのルートに
docker-compose.yml
ファイルを作成します。ここにPostgresイメージを追加し、環境の認証情報を指定します。
# 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
にマッピングされます。これはデータベースが外部に公開されるポート番号になります。 - データベースのユーザー認証情報が設定され、データベースに名前が付けられます。
- コンテナ内のデータベースに接続するには、
docker-compose.yml
ファイルで定義された認証情報を使用して新しい接続文字列を作成します。例えば
DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests"
上記の.env.test
ファイルは、複数の.env
ファイル設定の一部として使用されます。複数の.env
ファイルを使用してプロジェクトをセットアップする方法の詳細については、複数.env
ファイルの使用セクションを参照してください。
- ターミナルタブを引き続き使用できるように、コンテナをデタッチモードで作成するには、次のコマンドを実行します
docker compose up -d
-
次に、コンテナ内で
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が表示されます。
-
前のステップで取得したコンテナ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)
統合テスト
統合テストは、本番環境や開発環境ではなく、専用のテスト環境内のデータベースに対して実行されます。
操作の流れ
これらのテストを実行する流れは次のとおりです
- コンテナを起動し、データベースを作成する
- スキーマをマイグレートする
- テストを実行する
- コンテナを破棄する
各テストスイートは、すべてのテストが実行される前にデータベースにデータをシードします。スイート内のすべてのテストが終了した後、すべてのテーブルのデータは削除され、接続は終了されます。
テストする関数
テストしているeコマースアプリケーションには、注文を作成する関数があります。この関数は以下を実行します
- 注文する顧客に関する入力を受け入れる
- 注文される製品に関する入力を受け入れる
- 顧客が既存のアカウントを持っているか確認する
- 製品が在庫にあるか確認する
- 製品が存在しない場合、「在庫切れ」メッセージを返す
- 顧客がデータベースに存在しない場合、アカウントを作成する
- 注文を作成する
このような関数の例は以下に示されています
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
が使用され、データベースのデータがクリアされます。
deleteMany
は、スキーマの構造を事前に把握している場合に十分である可能性があります。これは、操作がモデルのリレーションシップのセットアップ方法に従って正しい順序で実行される必要があるためです。
しかし、これはモデルをマッピングしてそれらにtruncateを実行する、より汎用的なソリューションほどスケーラブルではありません。そのようなシナリオや生SQLクエリの使用例については、「生SQL/TRUNCATE
ですべてのデータを削除する」を参照してください。
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
ファイルにスクリプトを追加して、データベースをセットアップし、テストを実行し、その後手動でコンテナを破棄することができます。
テストが機能しない場合は、このブログで説明されているように、テストデータベースが適切にセットアップされ、準備ができていることを確認する必要があります。
"scripts": {
"docker:up": "docker compose up -d",
"docker:down": "docker compose down",
"test": "yarn docker:up && yarn prisma migrate deploy && jest -i"
},
test
スクリプトは以下を実行します
docker compose up -d
を実行して、Postgresイメージとデータベースを含むコンテナを作成します。./prisma/migrations/
ディレクトリにあるマイグレーションをデータベースに適用し、これによりコンテナのデータベースにテーブルを作成します。- テストを実行します。
満足したら、yarn docker:down
を実行して、コンテナ、そのデータベース、およびすべてのテストデータを破棄できます。