prisma-mockを救いたい
皆さんPrismaは活用していますか?
\ハーイ/
そうですね、もちろん大いに活用していることかと思われます。
そんな便利ライブラリPrismaですがテストを書こうとなるとちょっと困ったことになります。
今回はprisma-mockの紹介とPRを投げた話について書いてみようと思います。
Prismaでテストをしようと思うときに困ること
- データを用意するのが面倒くさい
- Prismaが返すデータをMockImplementationしなければいけない
- ローカルでテスト用のDBを建てるのが面倒くさい
- DBの実機にデータを投げてテストをすると時間がかかる
ですね
データを用意するのが面倒くさい
@quramy/prisma-fabbrica
か @faker-js/faker
を使って頑張りましょう……
Prismaが返すデータをMockImplementationしなければいけない
一応公式曰く
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import prisma from './client'
jest.mock('./client', () => ({
__esModule: true,
default: mockDeep<PrismaClient>(),
}))
beforeEach(() => {
mockReset(prismaMock)
})
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
のようにPrismaのインスタンスに対してMock作ってねという事らしいですが
複数のテーブルが絡んだテストを書こうとか、何喧嘩レコードがある状態のテストを書こうとかするといちいち mockImplementation
やら mockImplementationOnce
やらで返り値を書いて~とかやらないといけなくなります。(なんか他にいい方法あったりするのだろうか?)
ローカルでテスト用のDBを建てるのが面倒くさい
それはそう
DBの実機にデータを投げてテストをすると時間がかかる
それはそう
prisma-mock
ということで2
も3
も4
も解決してくれる、そんなライブラリが prisma-mock
です
prisma-mock
の特徴としては、 インメモリでPrismaの挙動をシミュレーションする です。
この時点でわざわざ返り値を mockImplementation
で実装しなくてもいいし、インメモリなのでテスト実行時間が短くて済みます。あら素敵✨
実際これをどう使うかというと、以下の様にJestの準備段階でPrismaのインスタンスをprisma-mockのインスタンスに差し替えるだけで済みます。お手軽!!
import createPrismaMock from "prisma-mock"
let client
beforeEach(() => {
client = createPrismaMock()
})
あとはテストデータを何とか用意して戻り値を確認すればテストの出来上がりです。すばらしい✌💪
自分はこれを実際のコードにContextとして引き回してDIしています。
複合Unique制約を使ったconnectがエラーになる
こんな素晴らしいライブラリですが、実はまだ開発中でして一部の機能が実装されていなかったりします。
自分のケースでは
- 接続先のレコードが既に存在している
- 新しいレコードを作成する際に複合Unique Indexを介して既存レコードに接続したい
という複合条件の際にコケてしまいました。
具体的には以下のようなスキーマがあり、Petレコードに複合Indexのname_ownerIdを介してToyを接続したい場合に発生します
model Pet {
id Int @id @default(autoincrement())
name String
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
has Toy[]
@@unique([name, ownerId])
}
model Toy {
id Int @id @default(autoincrement())
name String
owner Pet? @relation(fields: [ownerId], references: [id])
ownerId Int
}
実行側のコードとしては以下の様になります。
const client = await createPrismaClient({})
const user = await client.user.create({
data: {
role: "USER",
name: "Bob",
uniqueField: "user",
pets: {
create: {
name: "John",
},
},
},
})
const toy = await client.toy.create({
data: {
name: "Ball",
owner: {
connect: {
name_ownerId: {
name: "John",
ownerId: 1,
},
},
},
},
})
これを実行すると以下の部分でエラーが発生してしまいます。
connections.forEach((connect, idx) => {
const keyToMatch = Object.keys(connect)[0]
const keyToGet = field.relationToFields[0]
const targetKey = field.relationFromFields[0]
if (keyToGet && targetKey) {
let connectionValue = connect[keyToGet]
if (keyToMatch !== keyToGet) {
const valueToMatch = connect[keyToMatch]
const matchingRow = data[getCamelCase(field.type)].find(
(row) => {
return row[keyToMatch] === valueToMatch
}
)
if (!matchingRow) {
throwUnkownError(
"An operation failed because it depends on one or more records that were required but not found. {cause}"
)
}
connectionValue = matchingRow[keyToGet]
原因としてはconnectに指定されたname_ownerId
をそのままカラム名としてルックアップ仕様としてしまっている為なので、これを以下の様に修正します。
connections.forEach((connect, idx) => {
const keyToMatch = Object.keys(connect)[0]
const keyToGet = field.relationToFields[0]
const targetKey = field.relationFromFields[0]
if (keyToGet && targetKey) {
let connectionValue = connect[keyToGet]
if (keyToMatch !== keyToGet) {
const valueToMatch = connect[keyToMatch]
let matchingRow = data[getCamelCase(field.type)].find(
(row) => {
return row[keyToMatch] === valueToMatch
}
)
if (!matchingRow) {
const refModel = datamodel.models.find(
(model) =>
getCamelCase(field.type) === getCamelCase(model.name)
)
const uniqueIndexes = refModel.uniqueIndexes.map(
(index) => {
return {
...index,
key: index.name ?? index.fields.join("_"),
}
}
)
const indexKey = uniqueIndexes.find(
(index) => index.key === keyToMatch
)
matchingRow = data[getCamelCase(field.type)].find(
(row) => {
const target = Object.fromEntries(
Object.entries(row).filter(
(row) =>
indexKey?.fields.includes(row[0]) ?? false
)
)
return shallowCompare(target, valueToMatch)
}
)
if (!matchingRow) {
throwUnkownError(
"An operation failed because it depends on one or more records that were required but not found. {cause}"
)
}
}
connectionValue = matchingRow[keyToGet]
やっていることとしては単純にルックアップが失敗した後にインデックスを拾ってきて名前が一致したらそれで拾ってくるだけですね
この修正はPRを出しておきました(めちゃくちゃざっくり書いたので通るだろうか…)
おわりに
まだまだ開発途中ではあるのですがこれが十分使えるようになるとPrismaのテストがめちゃくちゃ楽になるので今後も注視していきたいですね。
Discussion