본문 바로가기

Dev.

테스트도 전략이다

출처 - fauxels의 사진 in pixel

회사에서 E2E를 도입해본 경험이 있습니다. 그 당시 테스트라는 영역이 몇가지 있고 그 중 하나가 E2E라는 개념이 있다고만 알고 있었습니다. 하지만 각 테스트 영역은 Trade Off가 존재하고, 조직의 상황에 맞게 전략적인 선택을 할 수 있다는 걸 알게되었습니다. 이 글을 통해 지식을 공유합니다.

 

목차

  • 목적과 용도에 맞게 테스트를 적용하자
  • 테스트 유형
  • 예제 코드
  • 트레이드 오프 (Trade Off)
  • 마무리

연재물

 


목적과 용도에 맞게 테스트를 적용하자

테스트 코드는 여러 유형이 있습니다. 각 테스트 코드들은 목적, 방법 등이 다르고,

 

🤔 ‘ 어떤 테스트 코드에 집중하냐 ’

 

에 따라서 Trade Off 가 있습니다. 그렇기 때문에 각 테스트 코드의 유형을 이해하고 현재의 조직 상황에 맞게 적절한 테스트 전략을 취할 필요가 있습니다.

일반적으로 자주 사용되는 테스트 코드 유형에는 전구간(E2E) 테스트, 통합(Integration)테스트, 단위(Unit) 테스트. 정적(Static) 테스트 등이 있습니다. 더 나아가 지속적인 통합과 배포를 위한 자동화된 테스트 코드를 작성하는 것도 중요합니다.

하지만 여기서는 자동화된 테스트 코드에 대해서는 논의하지 않겠습니다.

 

 

 


테스트 유형

먼저 각 테스트 유형을 좀 더 살펴 보도록 하죠

전 구간(=E2E) 테스트

  • 시스템의 전반적인 영역을 테스트 합니다.
  • 실제 사용자 환경에서, 사용자 입장으로 테스트를 수행합니다. (이 개념은 인수테스트와 동일하게 여겨집니다)
  • 대표적인 Tool 이 Cypress, Playwright, WebDriverIO 입니다.

통합(Integration) 테스트

  • 주로 모듈간의 상호 작용이 올바르게 되는지 테스트합니다.
  • 외부 라이브러리 까지 묶어 테스트를 합니다.
  • 대표적인 Tool 이 Testing-Library, Enzma 등등이 있습니다.

단위(Unit) 테스트

  • 기능들이 각각 독립적으로 잘 동작하는지 테스트 합니다.
  • 가장 작은 단위의 테스트 입니다. 주로 함수 레벨에서 이뤄집니다.
  • 대표적인 Tool 이 Jest, Mocha 등등이 있습니다.

정적(Static) 테스트

  • 타입오류, 린트오류 체크 등이 이 정적 테스트에 해당합니다.
  • ESLint, Typescript의 tsc 등등이 여기에 해당합니다.

아마 테스트 개념을 처음 접하시는 분들은 위의 텍스트를 읽어도 감이 안오실 수 있습니다. 좀 더 차이점을 명확하게 느끼기 위해서

 

아래 그림을 보겠습니다.

이 그림은 각 테스트를 잘 표현해주는 그림입니다.

 

 

예제코드

이번엔 각 테스트코드 유형마다 예제를 들어보죠

 

전 구간(E2E) 테스트

일반적으로 전체 어플리케이션을 실행하며, 테스트는 실제 사용자가 앱을 사용하는 것처럼 앱과 상호작용 합니다.

아래 코드는 playwright 로 작성된 e2e 테스트 코드입니다.

test("when the filter of items was selected", async ({ page }) => {
  const itemsPage = new Items(page)
  await itemsPage.itemBtnInHeader.click()

  // when
  await itemsPage.chargedItemsInDanjiBtn.click()
  await itemsPage.openDropdown("담당단지매물", "담당단지 매물")

  // then
  await expect(itemsPage.showRoomList()).toHaveCount(5)

  // when
  await itemsPage.chargedItemsInDanjiBtn.click()
  await itemsPage.openDropdown("담당단지매물", "담당아닌단지 매물")

  // then
  expect(itemsPage.showRoomList()).toHaveCount(0)
})

 

통합(Integration) 테스트

통합테스트모듈과 모듈간의 상호작용을 테스트 합니다. 저는 주로 단위테스트를 작성하면서 단위테스트가 더이상 단위테스트가 아닌 경우가 있었습니다.

특히 컴포넌트가 상위 컴포넌트일 수록 하위 컴포넌트를 포함하고 있다보니, 코드간 상호작용이 많아지는거죠. 이때부터는 단위테스트가 아닌 통합테스트로 접근해야합니다.

 

아래 코드는 이해를 돕기위해 약간 억지로 작성한 코드지만, 아래의 코드는 여러 모듈들과의 상호작용을 어떻게 테스트 한다는 건지 충분히 이해할 수 있습니다.

 

그리고 테스트 할 수 없는 것에 대해서는 mocking 을 하는 것이 올바른 접근 방법입니다.

import * as React from 'react'
import {screen, render, fireEvent} from '@testing-library/react'
import {rest} from 'msw'
import {setupServer} from 'msw/node'
import {handlers} from 'test/server-handlers'
import App from '../app'

const server = setupServer(...handlers)

beforeAll(() => server.listen())
afterAll(() => server.close())
afterEach(() => server.resetHandlers())

test(`input the cost & displays the cost`, async () => {
  const screen = await render(<App />, {route: '/cost'})
  const input = screen.getByLabelText('cost-input')
  const button = screen.getByRole('button', { name: 'submit' })

	fireEvent.change(input, {target: {value: '23'}})
	fireEvent.click(button)

  await waitForElementToBeRemoved(() => screen.getByLabelText(/loading/i))

  expect(screen.getByText('cost')).toBeInTheDocument()
})

 

단위(Unit) 테스트

단위테스트에 정의는 더 쉽습니다. 주로 함수단위로 이뤄질 때가 많으며, 하나의 컴포넌트 테스트도 단위(Unit)테스트라고 볼 수 있습니다.

test('renders the items in a list', () => {
  const screen = render(<ItemList items={['대림', '삼성레미안']} />)

  expect(screen.getByText("대림")).toBeInTheDocument()
  expect(screen.getByText("삼성레미안").toBeInTheDocument()
  expect(screen.queryByText("두산")).not.toBeInTheDocument()
})

 

정적(Static) 테스트

lint 는 .eslintrc.ts 에 설정해주고, type 은 tsconfig.json 에 정의해준다면, 버그를 유발할 수 있는 코드를 미리 잡아 줄 것입니다.

 

 

트레이드오프 (Trade Off)

하지만 이 트로피 그림이 중요한 이유는 따로 있습니다. Trade Off 개념인데요. 우리가 이 모든 테스트를 작성하고 관리할 수 있다면, 얼마나 좋을까요.

하지만 현실은 항상 여유가 있지 않습니다. 우리가 서비스를 개발하는 개발자 일 수록 배포 일정을 무시할 수 없습니다. 제품이 시장에 나가야하는 일정도 고려해야합니다.

 

게다가 각 테스트 구간은 선택에 따라 장단점이 있습니다.

 

비용측면

상위로 갈수록 테스트 비용은 증가합니다.

  • 테스트를 진행하는데 있어서 실제 컴퓨팅 자원을 많이 잡아먹습니다.
  • 테스트를 작성하고 유지 관리하는 수고로움도 올라 갑니다.

 


속도측면

위로 올라갈 수록 테스트 속도는 느려집니다. 특히 e2e 는 사용자가 실제 사용하는 흐름을 만들어야 하기 때문에 느릴 수 밖에 없습니다. 그래서 cypress와 playwright 는 테스트를 병렬 처리하는 기능을 제공합니다.

(cypress는 이 기능이 유료입니다)

 


테스트 신뢰도

테스트에 신뢰도는 위로 올라갈 수록 높아집니다. 즉 상위로 올라 갈 수록 비용, 속도가 높아지지만 그만큼 테스트의 신뢰도가 높아집니다. 그 이유는 테스트가 사용자의 사용 방식과 유사할 수록 테스트 결과에 대한 신뢰도는 더 높을 수 밖에 없습니다.

단위(unit)테스트를 제 아무리 많이 작성한다고 해도, 각 단위가 합쳐졌을 때는 또 어떤 결과를 내뱉을지 모르기 때문입니다.

 

 

 

마무리

테스트 코드는 각 유형마다 Trade Off 를 가지고 있습니다. 결국 조직의 현 상황에서 어떤 목표를 최우선으로 하냐에 따라서 테스트 전략은 달라질 수 있습니다.

 

🚧 당장 조직 내에서 테스트의 필요성을 느끼지 않는다면,

  • 단위테스트 부터 작업자 판단에 의해 시작해본다.
  • 기대성과는 서비스 품질 보다는 테스트 친화적인 코드 구축에 더 초점을 둔다.

🤝 조직 내에서 테스트 구축을 위한 일정이 주어진다면,

  • 사용자 시나리오에 가까운 E2E 테스트를 구축한다.
  • 테스트 신뢰도를 높여서, 서비스 품질을 향상시키는데 초점을 둔다.

 

또한 테스트 코드는 테스트만을 위한 역할만 있는게 아닙니다. 단위(Unit) 테스트는 TDD 라는 개발 방법론에서 유용하게 사용됩니다. 이 때 단순히 테스트 코드로서의 역할이 아니라 설계를 돕는 역할 까지 하게 됩니다.

 

TDD에 대한 글을 써놨는데요. 시간되시면 한번 봐보시고 의견 남겨주세요.

TDD는 코드 설계를 위한 도구이다.(링크)

 

다음 글들에서는 실무에서 E2E 테스트를 접목해본 내용을 다뤄보도록 하겠습니다.

 

 
 

아래는 개발 커뮤니티 링크입니다. 들어오셔서 기술 정보, 트렌드 정보, 유료스터디, 무료스터디, 모각코 등등을 이용해 보세요.