Skip to content

Commit

Permalink
feat(be): implement transaction rollback in unit test (#1562)
Browse files Browse the repository at this point in the history
* feat(be): implement transaction extension for prisma client
prisma/prisma-client-extensions#47 참고하여 작성했습니다

* feat(be): add transaction extension on index ts file

* feat(be): implement transaction rollback for group service unit test

* test(be): add comments and type

* test(be): add await keyword

* test(be): add chai exclude

* test(be): delete override prisma service func

* test(be): increase timeout for before each hook

* test(be): disable timeout for before each hook

* test(be): add comment

* test(be): fix comment
  • Loading branch information
gyunseo authored Mar 18, 2024
1 parent aa465fd commit f96a3cf
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 46 deletions.
67 changes: 21 additions & 46 deletions apps/backend/apps/client/src/group/group.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CACHE_MANAGER } from '@nestjs/cache-manager'
import { ConfigService } from '@nestjs/config'
import { Test, type TestingModule } from '@nestjs/testing'
import { Prisma } from '@prisma/client'
import { Prisma, PrismaClient } from '@prisma/client'
import type { Cache } from 'cache-manager'
import { expect } from 'chai'
import * as chai from 'chai'
Expand All @@ -12,21 +12,29 @@ import {
ConflictFoundException,
EntityNotExistException
} from '@libs/exception'
import { PrismaService } from '@libs/prisma'
import { PrismaService, type FlatTransactionClient } from '@libs/prisma'
import { transactionExtension } from '@libs/prisma'
import { GroupService } from './group.service'
import type { UserGroupData } from './interface/user-group-data.interface'

chai.use(chaiExclude)

describe('GroupService', () => {
let service: GroupService
let cache: Cache
let prisma: PrismaService
beforeEach(async () => {
let tx: FlatTransactionClient

const prisma = new PrismaClient().$extends(transactionExtension)

beforeEach(async function () {
// TODO: CI 테스트에서 timeout이 걸리는 문제를 우회하기 위해서 timeout을 0으로 설정 (timeout disabled)
// local에서는 timeout을 disable 하지 않아도 테스트가 정상적으로 동작함 (default setting: 2000ms)
this.timeout(0)
//transaction client
tx = await prisma.$begin()
const module: TestingModule = await Test.createTestingModule({
providers: [
GroupService,
PrismaService,
{ provide: PrismaService, useValue: tx },
ConfigService,
{
provide: CACHE_MANAGER,
Expand All @@ -39,7 +47,6 @@ describe('GroupService', () => {
}).compile()
service = module.get<GroupService>(GroupService)
cache = module.get<Cache>(CACHE_MANAGER)
prisma = module.get<PrismaService>(PrismaService)
})

it('should be defined', () => {
Expand Down Expand Up @@ -166,9 +173,8 @@ describe('GroupService', () => {
describe('joinGroupById', () => {
let groupId: number
const userId = 4

beforeEach(async () => {
const group = await prisma.group.create({
const group = await tx.group.create({
data: {
groupName: 'test',
description: 'test',
Expand All @@ -182,26 +188,7 @@ describe('GroupService', () => {
})

afterEach(async () => {
try {
await prisma.userGroup.delete({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
userId_groupId: { userId, groupId }
}
})
} catch {
/* 삭제할 내용이 없는 경우 예외 무시 */
}

try {
await prisma.group.delete({
where: {
id: groupId
}
})
} catch {
/* 삭제할 내용 없을 경우 예외 무시 */
}
await tx.$rollback()
})

it('should return {isJoined: true} when group not set as requireApprovalBeforeJoin', async () => {
Expand All @@ -225,7 +212,7 @@ describe('GroupService', () => {
})

it('should return {isJoined: false} when group set as requireApprovalBeforeJoin', async () => {
await prisma.group.update({
await tx.group.update({
where: {
id: groupId
},
Expand All @@ -250,7 +237,7 @@ describe('GroupService', () => {
})

it('should throw ConflictFoundException when user is already group memeber', async () => {
await prisma.userGroup.create({
await tx.userGroup.create({
data: {
userId,
groupId,
Expand All @@ -270,7 +257,7 @@ describe('GroupService', () => {
{ userId, expiresAt: Date.now() + JOIN_GROUP_REQUEST_EXPIRE_TIME }
])

await prisma.group.update({
await tx.group.update({
where: {
id: groupId
},
Expand All @@ -291,9 +278,8 @@ describe('GroupService', () => {
describe('leaveGroup', () => {
const groupId = 3
const userId = 4

beforeEach(async () => {
await prisma.userGroup.createMany({
await tx.userGroup.createMany({
data: [
{
userId,
Expand All @@ -310,18 +296,7 @@ describe('GroupService', () => {
})

afterEach(async () => {
try {
await prisma.userGroup.deleteMany({
where: {
OR: [
{ AND: [{ userId }, { groupId }] },
{ AND: [{ userId: 5 }, { groupId }] }
]
}
})
} catch {
return
}
await tx.$rollback()
})

it('should return deleted userGroup when valid userId and groupId passed', async () => {
Expand Down
1 change: 1 addition & 0 deletions apps/backend/libs/prisma/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './prisma.module'
export * from './prisma.service'
export * from './transaction.extension'
71 changes: 71 additions & 0 deletions apps/backend/libs/prisma/src/transaction.extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Prisma } from '@prisma/client'
import { PrismaService } from './prisma.service'

export type FlatTransactionClient = Prisma.TransactionClient & {
$commit: () => Promise<void>
$rollback: () => Promise<void>
}

const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true }

export const transactionExtension = Prisma.defineExtension({
client: {
async $begin() {
const prisma = Prisma.getExtensionContext(this)
let setTxClient: (txClient: Prisma.TransactionClient) => void
let commit: () => void
let rollback: () => void

// a promise for getting the tx inner client
const txClient = new Promise<Prisma.TransactionClient>((res) => {
setTxClient = res
})

// a promise for controlling the transaction
const txPromise = new Promise((_res, _rej) => {
commit = () => _res(undefined)
rollback = () => _rej(ROLLBACK)
})

// opening a transaction to control externally
if (
'$transaction' in prisma &&
typeof prisma.$transaction === 'function'
) {
const tx = prisma
.$transaction((txClient) => {
setTxClient(txClient as unknown as Prisma.TransactionClient)
return txPromise
})
.catch((e) => {
if (e === ROLLBACK) {
return
}
throw e
})

// return a proxy TransactionClient with `$commit` and `$rollback` methods
return new Proxy(await txClient, {
get(target, prop) {
if (prop === '$commit') {
return () => {
commit()
return tx
}
}
if (prop === '$rollback') {
return () => {
rollback()
return tx
}
}
return target[prop as keyof typeof target]
}
}) as FlatTransactionClient
}

throw new Error('Transactions are not supported by this client')
},
getPaginator: PrismaService.prototype.getPaginator
}
})

0 comments on commit f96a3cf

Please sign in to comment.