Skip to content

Commit f96a3cf

Browse files
authored
feat(be): implement transaction rollback in unit test (#1562)
* 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
1 parent aa465fd commit f96a3cf

File tree

3 files changed

+93
-46
lines changed

3 files changed

+93
-46
lines changed

apps/backend/apps/client/src/group/group.service.spec.ts

Lines changed: 21 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { CACHE_MANAGER } from '@nestjs/cache-manager'
22
import { ConfigService } from '@nestjs/config'
33
import { Test, type TestingModule } from '@nestjs/testing'
4-
import { Prisma } from '@prisma/client'
4+
import { Prisma, PrismaClient } from '@prisma/client'
55
import type { Cache } from 'cache-manager'
66
import { expect } from 'chai'
77
import * as chai from 'chai'
@@ -12,21 +12,29 @@ import {
1212
ConflictFoundException,
1313
EntityNotExistException
1414
} from '@libs/exception'
15-
import { PrismaService } from '@libs/prisma'
15+
import { PrismaService, type FlatTransactionClient } from '@libs/prisma'
16+
import { transactionExtension } from '@libs/prisma'
1617
import { GroupService } from './group.service'
1718
import type { UserGroupData } from './interface/user-group-data.interface'
1819

1920
chai.use(chaiExclude)
20-
2121
describe('GroupService', () => {
2222
let service: GroupService
2323
let cache: Cache
24-
let prisma: PrismaService
25-
beforeEach(async () => {
24+
let tx: FlatTransactionClient
25+
26+
const prisma = new PrismaClient().$extends(transactionExtension)
27+
28+
beforeEach(async function () {
29+
// TODO: CI 테스트에서 timeout이 걸리는 문제를 우회하기 위해서 timeout을 0으로 설정 (timeout disabled)
30+
// local에서는 timeout을 disable 하지 않아도 테스트가 정상적으로 동작함 (default setting: 2000ms)
31+
this.timeout(0)
32+
//transaction client
33+
tx = await prisma.$begin()
2634
const module: TestingModule = await Test.createTestingModule({
2735
providers: [
2836
GroupService,
29-
PrismaService,
37+
{ provide: PrismaService, useValue: tx },
3038
ConfigService,
3139
{
3240
provide: CACHE_MANAGER,
@@ -39,7 +47,6 @@ describe('GroupService', () => {
3947
}).compile()
4048
service = module.get<GroupService>(GroupService)
4149
cache = module.get<Cache>(CACHE_MANAGER)
42-
prisma = module.get<PrismaService>(PrismaService)
4350
})
4451

4552
it('should be defined', () => {
@@ -166,9 +173,8 @@ describe('GroupService', () => {
166173
describe('joinGroupById', () => {
167174
let groupId: number
168175
const userId = 4
169-
170176
beforeEach(async () => {
171-
const group = await prisma.group.create({
177+
const group = await tx.group.create({
172178
data: {
173179
groupName: 'test',
174180
description: 'test',
@@ -182,26 +188,7 @@ describe('GroupService', () => {
182188
})
183189

184190
afterEach(async () => {
185-
try {
186-
await prisma.userGroup.delete({
187-
where: {
188-
// eslint-disable-next-line @typescript-eslint/naming-convention
189-
userId_groupId: { userId, groupId }
190-
}
191-
})
192-
} catch {
193-
/* 삭제할 내용이 없는 경우 예외 무시 */
194-
}
195-
196-
try {
197-
await prisma.group.delete({
198-
where: {
199-
id: groupId
200-
}
201-
})
202-
} catch {
203-
/* 삭제할 내용 없을 경우 예외 무시 */
204-
}
191+
await tx.$rollback()
205192
})
206193

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

227214
it('should return {isJoined: false} when group set as requireApprovalBeforeJoin', async () => {
228-
await prisma.group.update({
215+
await tx.group.update({
229216
where: {
230217
id: groupId
231218
},
@@ -250,7 +237,7 @@ describe('GroupService', () => {
250237
})
251238

252239
it('should throw ConflictFoundException when user is already group memeber', async () => {
253-
await prisma.userGroup.create({
240+
await tx.userGroup.create({
254241
data: {
255242
userId,
256243
groupId,
@@ -270,7 +257,7 @@ describe('GroupService', () => {
270257
{ userId, expiresAt: Date.now() + JOIN_GROUP_REQUEST_EXPIRE_TIME }
271258
])
272259

273-
await prisma.group.update({
260+
await tx.group.update({
274261
where: {
275262
id: groupId
276263
},
@@ -291,9 +278,8 @@ describe('GroupService', () => {
291278
describe('leaveGroup', () => {
292279
const groupId = 3
293280
const userId = 4
294-
295281
beforeEach(async () => {
296-
await prisma.userGroup.createMany({
282+
await tx.userGroup.createMany({
297283
data: [
298284
{
299285
userId,
@@ -310,18 +296,7 @@ describe('GroupService', () => {
310296
})
311297

312298
afterEach(async () => {
313-
try {
314-
await prisma.userGroup.deleteMany({
315-
where: {
316-
OR: [
317-
{ AND: [{ userId }, { groupId }] },
318-
{ AND: [{ userId: 5 }, { groupId }] }
319-
]
320-
}
321-
})
322-
} catch {
323-
return
324-
}
299+
await tx.$rollback()
325300
})
326301

327302
it('should return deleted userGroup when valid userId and groupId passed', async () => {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './prisma.module'
22
export * from './prisma.service'
3+
export * from './transaction.extension'
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { Prisma } from '@prisma/client'
2+
import { PrismaService } from './prisma.service'
3+
4+
export type FlatTransactionClient = Prisma.TransactionClient & {
5+
$commit: () => Promise<void>
6+
$rollback: () => Promise<void>
7+
}
8+
9+
const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true }
10+
11+
export const transactionExtension = Prisma.defineExtension({
12+
client: {
13+
async $begin() {
14+
const prisma = Prisma.getExtensionContext(this)
15+
let setTxClient: (txClient: Prisma.TransactionClient) => void
16+
let commit: () => void
17+
let rollback: () => void
18+
19+
// a promise for getting the tx inner client
20+
const txClient = new Promise<Prisma.TransactionClient>((res) => {
21+
setTxClient = res
22+
})
23+
24+
// a promise for controlling the transaction
25+
const txPromise = new Promise((_res, _rej) => {
26+
commit = () => _res(undefined)
27+
rollback = () => _rej(ROLLBACK)
28+
})
29+
30+
// opening a transaction to control externally
31+
if (
32+
'$transaction' in prisma &&
33+
typeof prisma.$transaction === 'function'
34+
) {
35+
const tx = prisma
36+
.$transaction((txClient) => {
37+
setTxClient(txClient as unknown as Prisma.TransactionClient)
38+
return txPromise
39+
})
40+
.catch((e) => {
41+
if (e === ROLLBACK) {
42+
return
43+
}
44+
throw e
45+
})
46+
47+
// return a proxy TransactionClient with `$commit` and `$rollback` methods
48+
return new Proxy(await txClient, {
49+
get(target, prop) {
50+
if (prop === '$commit') {
51+
return () => {
52+
commit()
53+
return tx
54+
}
55+
}
56+
if (prop === '$rollback') {
57+
return () => {
58+
rollback()
59+
return tx
60+
}
61+
}
62+
return target[prop as keyof typeof target]
63+
}
64+
}) as FlatTransactionClient
65+
}
66+
67+
throw new Error('Transactions are not supported by this client')
68+
},
69+
getPaginator: PrismaService.prototype.getPaginator
70+
}
71+
})

0 commit comments

Comments
 (0)