avatar
Published on

NestJS ์—์„œ E2E TEST ๐Ÿƒ๐Ÿปโ€โ™€๏ธ

Authors
  • avatar
    Name
    Haneul
    Twitter

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 ์ดˆ๊ธฐํ™” ๋ฐฉ๋ฒ•์€ ๋ชจ๋‘ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

afterAll

  • ํ…Œ์ŠคํŠธ๋ฅผ ๋งค๋ฒˆ ์ƒˆ๋กœ ์‹œ์ž‘ํ•  ๋•Œ๋งˆ๋‹ค ๋งŒ๋“  ๋ฐ์ดํ„ฐ๋“ค์ด ์Œ“์—ฌ ์žˆ์œผ๋ฉด ๋‹ค์Œ ํ…Œ์ŠคํŠธ์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ธฐ ๋•Œ๋ฌธ์—, 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.')
      })
  })
})

์ฐธ๊ณ  :

supertest, nextJS ๊ณต์‹๋ฌธ์„œ,