- Published on
NestJS ์์ E2E TEST ๐๐ปโโ๏ธ
- Authors
- Name
- Haneul
E2E TEST ๐๐ปโโ๏ธ
E2E Test (End To End Test) ๋ ?
๊ฐ๋ณ ๋ชจ๋ ๋ฐ ํด๋์ค์ ์ค์ ์ ๋๋ ๋จ์ ํ ์คํธ์ ๋ฌ๋ฆฌ e2e ํ ์คํธ๋ ์ต์ข ์ฌ์ฉ์๊ฐ ํ๋ก๋์ ๊ณผ ํจ๊ป ํ๊ฒ ๋ ์ํธ ์์ฉ์ ์ข ๋ฅ์ ๋ ๊ฐ๊น์ด ๋ณด๋ค ์ข ํฉ์ ์ธ ์์ค์์ ํด๋์ค์ ๋ชจ๋์ ์ํธ ์์ฉ์ ๋ค๋ฃน๋๋ค. ๊ฐ API ์๋ํฌ์ธํธ์ ์ข ๋จ ๊ฐ ํ ์คํธ๋ ์์คํ ์ ์ ๋ฐ์ ์ธ ๋์์ด ์ ํํ๊ณ ํ๋ก์ ํธ ์๊ตฌ ์ฌํญ์ ์ถฉ์กฑํ๋์ง ํ์ธํ๋ ๋ฐ ๋์์ด ๋ฉ๋๋ค. NestJS๋ฅผ ์ฌ์ฉํ๋ฉด supertest ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ HTTP ์์ฒญ์ ์ฝ๊ฒ ์๋ฎฌ๋ ์ด์ ํ ์ ์์ต๋๋ค.
- Endpoint(์ข ๋จ) ๊ฐ ํ ์คํธ๋ก ์ฌ์ฉ์์ ์ ์ฅ์์ ์ฌ์ฉ์๊ฐ ์ฌ์ฉํ๋ ์ํฉ์ ๊ฐ์ ํ๊ณ ํ ์คํธ ํ๋ ๊ฒ
- ์ผ๋ฐ์ ์ผ๋ก ์น์ด๋ ์ดํ ๋ฑ์์ GUI๋ฅผ ํตํด ์๋๋ฆฌ์ค, ๊ธฐ๋ฅ ํ ์คํธ ๋ฑ์ ์ํํ๋ค.
- ์ฌ์ฉ์์๊ฒ ์ง์ ์ ์ผ๋ก ๋ ธ์ถ๋๋ ๋ถ๋ถ์ ์ ๊ฒํ๋ค.
- ์ ๋ ํ ์คํธ๋ก ๋ถ๊ฐ๋ฅํ ์ฌ์ฉ์ ๊ด์ ์ ํ ์คํธ๊น์ง ๊ฐ๋ฅํ๋ค.
- Endpoint ํ ์คํธ๋ฅผ ํต๊ณผํ๋ฉด ๊ธฐ๋ฅ์ด ์ ์๋ํ๋ค๋ ๊ฒ์ด๋ฏ๋ก ๋ชจ๋ ํ ์คํธ๋ฅผ ํ ์ ์๋ค๋ฉด E2E Test๋ง์ด๋ผ๋ ํ๋ ๊ฒ์ด ์ข๋ค!
- ๋ฐฑ์๋ ๊ด์ ์์ ๊ฐ๋ฐํ REST API๋ฅผ ํ ์คํธ ํ๊ธฐ ์ํด ์ค์ ๋ก ์๋ฒ์ ์์ฒญ์ ๋ณด๋ธ ๋ค ํด๋ผ์ด์ธํธ์์ ์ํ๋ ๋ฐ์ดํฐ๊ฐ ์ ์ก๋๋ ์ง ํ์ธํด์ผ ํ๋ค.
E2E ํ ์คํธ๋ ํ๊ฒฝ์ ์์กดํ๋ ํ ์คํธ์ด์ง๋ง, ๋จ์ ํ ์คํธ๋ ์คํ ์ค์ธ ํ๊ฒฝ์ ์์กดํ๋ฉด ์ ๋๊ณ , ๋น ๋ฅด๊ฒ ์คํ๋์ด์ผ ํ๋ค. E2E ํ ์คํธ์์๋ ๋ณดํต ํ ์คํธ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ๊ณ , ํ ์คํธ์ ์ ๋ขฐ์ฑ์ด ๋์ง๋ง ์๋๊ฐ ๋๋ฆฌ๋ค๋ ๋จ์ ์ด ์๋ค.
NestJS
NestJS์์๋ ํน์ ๋๊ตฌ๋ฅผ ๊ฐ์ ํ์ง๋ ์์ง๋ง Jest๋ฅผ ๊ธฐ๋ณธ ํ ์คํธ ํ๋ ์์ํฌ๋ก ์ ๊ณตํด์ฃผ๋ฉฐ ํ ์คํ ํจํค์ง๋ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ ๊ฐ๋ฐ์๊ฐ ๋ค๋ฅธ ๋๊ตฌ๋ฅผ ์ฐพ๋๋ฐ ์๋ชจํ๋ ๋ฆฌ์์ค๋ฅผ ์ค์ผ ์ ์๋ค.
ํ ์คํธ ์ค์ ํ๊ธฐ
let podcastsRepository: Repository<Podcast>
let episodesRepository: Repository<Episode>
let usersRepository: Repository<User>
ํ ์คํธ ์ค์ ์ let์ผ๋ก repository๋ค์ด ์ด๊ธฐํ๋์ด์๋๋ฐ, ๋ฐ ๋ผ์ธ์ beforeAll์์ app๊ณผ repository๋ฅผ ์ด๊ธฐํ์ํค๊ธฐ ๋๋ฌธ์ let์ผ๋ก ์ ์ธ๋์ด ์์ต๋๋ค.
- repository ์ด๊ธฐํ
podcastsRepository = moduleFixture.get<Repository<Podcast>>(getRepositoryToken(Podcast))
๋๋จธ์ง episodeRepsitory, usersRepository ์ด๊ธฐํ ๋ฐฉ๋ฒ์ ๋ชจ๋ ๋์ผํฉ๋๋ค.
- ํ ์คํธ๋ฅผ ๋งค๋ฒ ์๋ก ์์ํ ๋๋ง๋ค ๋ง๋ ๋ฐ์ดํฐ๋ค์ด ์์ฌ ์์ผ๋ฉด ๋ค์ ํ ์คํธ์ ์ํฅ์ ๋ฏธ์น๊ธฐ ๋๋ฌธ์, databse์ drop ๋ช ๋ น์ ๋ด๋ ค DB๋ฅผ ์ด๊ธฐํ์ํค๋ ๊ณผ์ ์ด ํ์ํฉ๋๋ค. ๊ทธ๋์ afterAll ์์์ ์ด ๊ณผ์ ์ ์ํํ๋๋ก ํฉ๋๋ค.
ํ ์คํธ ์ํ
e2eํ ์คํธ๋ ํํ ์ข ๋จ๊ฐ ํ ์คํธ๋ก ๋ฒ์ญ์ด ๋๊ณ , ์ค์ ์ฌ์ฉ์๊ฐ ํ๋ํ๋ ๋ฐฉ์์ผ๋ก ํ ์คํธ๋ฅผ ํ๋ค๊ณ ์๊ฐํ์๋ฉด ๋ฉ๋๋ค. unit ํ ์คํธ์ฒ๋ผ ๊ฐ์ง ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ์ฌ์ฉํ์ง ์์ต๋๋ค.
const baseTest = () =request(app.getHttpServer()).post(GRAPHQL_ENDPOINT)
const publicTest = (query: string) => baseTest().send({ query })
const privateTest = (query: string) => baseTest().set('X-JWT', jwtToken).send({ query })
e2eํ
์คํธ์์๋ request(app.getHttpServer()).post(GRAPHQL_ENDPOINT).send({query})
๊ฐ ๊ณ์ ๋ฐ๋ณต๋ ๊ฒ์ด๋ฏ๋ก ํจ์๋ฅผ wrapping ํด๋์์ต๋๋ค. request๋ supertest ํจํค์ง์์ importํ ํจ์์ด๋ฉฐ, express test๋ฅผ ์ํด nestjs ํ๋ ์์ํฌ์ ํฌํจ ๋ ํจํค์ง์
๋๋ค
privateTest์ ๋ณด์๋ฉด set('X-JWT', jwtToken)๋ ํค๋์ 'X-JWT'๋ผ๋ ์ด๋ฆ์ผ๋ก jwtToken์ ๋๊ฒจ์ฃผ๋ ๊ณผ์ ์ ๋๋ค.
describe('Podcasts Resolver', () => {
describe('createPodcast', () => {
it('should create a new podcast', () =>
publicTest(`
mutation {
createPodcast(input: {
title: "${testPodcast.title}",
category: "${testPodcast.category}",
}) {
ok
id
}
}
`)
.expect(200)
.expect((res) => {
expect(res.body.data.createPodcast.ok).toBe(true)
expect(res.body.data.createPodcast.id).toBe(1)
}))
})
})
๋ํํด ๋์ ํจ์์ mutation ~ ๋ถ๋ถ์ ์ค์ ๋ก graphql ๋ฌธ์ด ๋ค์ด๊ฐ์์ต๋๋ค. ์ฃผ์ํ์ค ๋ถ๋ถ์ ${testPodcast.title}
์ด ๋ถ๋ถ์ธ๋ฐ ์๋ฐ์คํฌ๋ฆฝํธ์์๋ ์ ๋ ๊ฒ๋ง ํด๋ string์ผ๋ก ์ธ์ํ์ง๋ง, graphql์์๋ ๊ทธ๋ ์ง ์์ต๋๋ค. ์์ ๋ฐ์ดํ(')๋ ์๋๊ณ ํฐ ๋ฐ์ดํ(")๋ก ๊ฐ์ธ์ฃผ์ด์ผ ํ
์คํธ๋ก ์ธ์ํ๋ฏ๋ก ์ฃผ์ํ์
์ผ ํฉ๋๋ค.
๋ ํ๊ฐ์ง ์ฃผ์ํด์ผ ํ ์ ์ request.send...(์๋ฃจ์ ์ publicRest, privateTest) ์ด ์ฝ๋๋ค์ ๋ฐ๋์ ๋ฆฌํด๊ฐ์ผ๋ก ๋๊ฒจ์ฃผ์ ์ผ ํฉ๋๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด ํ ์คํธ๋ ๋ฌด์กฐ๊ฑด success๋ก ๋์ค๊ธฐ ๋๋ฌธ์ ์ฃผ์ํ์ ์ผ ํฉ๋๋ค. ๋๋จธ์ง ๊ฒฐ๊ณผ๋ค๋ ์ํฅ์ ๋ฐ์ ์ ์๊ธฐ ๋๋ฌธ์ ๊ผญ ๋ฆฌํด๊ฐ์ผ๋ก ๋๊ธฐ์ ์ผ ํ๋ค๋ ๊ฒ์ ๊ธฐ์ตํ์ ์ผ ํฉ๋๋ค.
expect(200): 200์ post ๋ฉ์๋๋ก sendํจ์๋ฅผ ์ด์ฉํ์ฌ ๋ณด๋ธ request์ ๋ํ ์๋ต์ status code๋ฅผ ์๋ฏธํฉ๋๋ค. /graphql์ ์ ์์ ์ผ๋ก query๊ฐ ์ ๋ณด๋ด์ก์ผ๋ฉด, 200 ์ ์๋ต์ฝ๋๋ฅผ ๋ฐ์์ผ ํ๋ค๋ ์๋ฏธ์ธ๋ฐ, ์์์ ์ธ๊ธํ ํฐ ๋ฐ์ดํ๋ฅผ ์๋ตํ๊ณ request ์์ฒญ์ ํ๋ฉด ํํ 400 ์๋ต์ฝ๋๋ฅผ ๋ง์ด ๋ฐ์ต๋๋ค.
ํ ์คํธ ๋ฐฉ๋ฒ์ unit ํ ์คํธ์ฒ๋ผ, ์ฑ๊ณต์ ์ผ๋ก ์ฒ๋ฆฌ ๋๋ ๊ฒฝ์ฐ, ์๋ฌ์ธ ๊ฒฝ์ฐ ์๋ฌ์ฒ๋ฆฌ ๋ฑ์ ๊ฒฝ์ฐ๊ฐ ์ ์ฒ๋ฆฌ๋๊ณ ์๋ ํ ์คํธ ํด๋ณด์๋ฉด ๋ฉ๋๋ค. unitํ ์คํธ์๋ ๋ฌ๋ฆฌ ์ค์ DB ์ฌ์ฉ, ์ค์ graphql์ query ์์ฒญ์ ํ๋ค๋ ์ ๋ฑ์ด ๋ค๋ฆ ๋๋ค.
describe('updatePodcast', () => {
const rating = 3
let podcastId: number
beforeAll(async () => {
const [podcast] = await podcastsRepository.find()
podcastId = podcast.id
})
it('should success to update Podcast', () => {
return publicTest(`
mutation {
updatePodcast(input: { id: ${podcastId}, payload: { rating: ${rating} } }) {
ok
}
}
`)
.expect(200)
.expect((res) => {
const {
body: {
data: {
updatePodcast: { ok },
},
},
} = res
expect(ok).toBe(true)
})
})
it('should failed to update Podcast, becuase of getPodcast emitting error', () => {
const errorPodcastId = 1000
return publicTest(`
mutation {
updatePodcast(input: { id: ${errorPodcastId}, payload: { rating: ${rating} } }) {
ok
error
}
}
`)
.expect(200)
.expect((res) => {
const {
body: {
data: {
updatePodcast: { ok, error },
},
},
} = res
expect(ok).toBe(false)
expect(error).toBe(`Podcast with id ${errorPodcastId} not found`)
})
})
it('should fail to update Podcast, due to invalid payload', () => {
const errorRating = 10
return publicTest(`
mutation {
updatePodcast(input: { id: ${podcastId}, payload: { rating: ${errorRating} } }) {
ok
error
}
}
`)
.expect(200)
.expect((res) => {
const {
body: {
data: {
updatePodcast: { ok, error },
},
},
} = res
expect(ok).toBe(false)
expect(error).toBe('Rating must be between 1 and 5.')
})
})
})
์ฐธ๊ณ :