[테스트 코드] 테스트의 기본 이해

개념

  • 테스트는 이름 그대로 소프트웨어를 테스트하는 작업이다.
  • 여러 명이 동시에 이용할 때에도 잘 견디는지 등의 문제들을 꼼꼼히 점검하는 일이다.
  • 테스트를 거치지 않은 소프트웨어는 일반적으로 신뢰하기 어렵다.
  • 실시간으로 언제 어디서 버그가 나올지 모른다.
  • 이때 테스트 코드를 잘 작성해 둔다면, 버그에 견고한 코드를 작성할 수 있다.
  • 같은 팀 개발자는 테스트를 보는 것만으로도 프로젝트의 전체적인 기능과 구조를 파악할 수 있게 된다.

테스트의 종류

  • 개발자가 가장 많이 마주하게 되는 테스트는 크게 세 가지로 아래와 같다.

image

유닛 테스트

  • 유닛(Unit)이라는 말 그대로, 가장 작은 단위의 테스트이다.
  • 단일 기능을 가지는 함수, 클래스의 메서드가 잘 작동하는지 확인한다.
  • 테스트하고자 하는 코드는 다른 외부 컴포넌트(웹 서버, DB 등)에 의존성이 없어야 한다.
  • 가장 간단하고, 직관적이며, 빠르게 실행과 결과를 볼 수 있는 테스트이다.

통합 테스트

  • 통합(Integration)이라는 말 그대로, 여러 요소를 통합한 테스트를 말한다.
  • 데이터베이스와 연동한 코드가 잘 작동하는지, 여러 함수와 클래스가 엮인 로직이 잘 작동하는지 등을 확인한다.
  • 유닛 테스트보다는 복잡하고 느리지만, 소프트웨어는 결국 여러 코드 로직의 통합이라는 점에서 통합 테스트 역시 중요하다.

E2E 테스트

  • E2E는 End To End의 약자로, 끝에서 끝, 즉 클라이언트 입장에서 테스트해보는 것이다.
  • 예를 들어 쇼핑몰 웹사이트의 경우, /login 으로 POST 요청 시 로그인은 잘 되는지, /order 로 POST 요청 시 주문 결과는 잘 나오는지 등을 확인한다.
  • 보통 유저 시나리오에 따라 테스트한다.
  • 테스트 중 가장 느리지만, 결국 소프트웨어를 사용하는건 유저이고, 유저 입장에서 해보는 테스트이므로, 역시 중요하다고 할 수 있다.

  • 보통 테스트는 유닛 -> 통합 -> E2E 순으로 작성하게 된다. (꼭 정답이 있는 건 아니다.)
  • 작은 단위부터 테스트를 작성하면서 점점 통합적인 테스트를 진행하게 된다.
  • 테스트 개수는 가장 작은 단위 테스트인 유닛 테스트가 가장 많고, E2E 테스트가 가장 적다.
    • 유닛 테스트로 추후에 어떠한 컴포넌트의 기능이 문제가 있는지 빠르게 찾아낼 수 있다.
    • 한편 통합테스트로 프로그램의 로직 흐름에 이상이 없는지를 파악할 수 있다.
    • E2E 테스트는 최종적으로 사용자 관점에서 사용하기에 기능적인 문제가 없는지 진행하는 테스트로 정리할 수 있다.

테스트 코드

  • 테스트 역시 코드로 구현할 수 있다.
  • 파이썬의 대표적인 테스트 프레임워크인 pytest를 사용할 것이다.
  • 우선 pytest를 설치한다.
1
$ pip install pytest
1
2
3
4
5
6
7
8
9
# test_example.py

# 테스트 대상이 되는 함수
def add(a: int, b: int) -> int:
    return a + b

# 테스트를 시행할 코드
def test_add():
    assert add(1, 1) == 2  # add(1, 1)의 출력이 2면 테스트를 통과합니다.
  • 이제 셸에서 pytest {파일 이름} 명령어로 위에서 작성한 테스트 코드를 실행시킬 수 있다.
1
2
3
4
5
6
7
8
$ python -m pytest test_example.py

========================== test session starts ==========================
platform darwin -- Python 3.8.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 -- 
cachedir: .pytest_cache
collected 1 item

test_example.py::test_add PASSED  # 테스트 통과에 성공
  • add(1, 1)의 출력이 2였기 때문에 테스트가 통과했다.
  • 만약 출력값을 2 가 아니라 3 이랑 같은지 비교하면 어떻게 될까?
1
2
3
4
5
6
7
# test_example.py

def add(a: int, b: int) -> int:
    return a + b

def test_add():
    assert add(1, 1) == 3  # 2 -> 3으로 수정
  • 그리고 다시 다음처럼 pytest 명령어를 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ pythom -m pytest test_example.py

========================== test session starts ==========================
platform darwin -- Python 3.8.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
collected 1 item

test_example.py F  # 테스트 통과에 실패                     [100%]

========================== FAILURES ==========================
========================== test_add ==========================

    def test_add():
>       assert add(1, 1) == 3
E       assert 2 == 3
E        +  where 2 = add(1, 1)

test_example.py:6: AssertionError
========================== short test summary info ==========================
FAILED test_example.py::test_add - assert 2 == 3
========================== 1 failed in 0.03s ==========================
  • 이번에는 F(Fail) 값을 내며 테스트가 실패했다고 나온다.
  • 동시에 코드의 어느 부분에서 테스트가 실패했는 지에 대한 정보가 나온다.
  • 이렇게 테스트를 구현하는 코드를 테스트 코드라고 흔히 부른다.

테스트 코드가 필요한 이유

  • 테스트 코드는 프로젝트의 코드를 테스트하기 위해 필요하다.
  • 하지만 테스트 코드는 단순히 테스트 실행 말고도 다음처럼 더 큰 의미들이 있다.

  • 테스트 코드는 코드가 동작하기 위해 필요한 것들과 입/출력을 드러낸다.
    • 테스트는 테스트하고자 하는 코드의 클라이언트 중심으로 작성한다.
    • 즉 테스트하고자 하는 코드를 사용하려면 어떤 의존성이 필요한지, 어떤 입력을 주면 어떤 출력을 뱉는지 테스트 코드를 보면 알 수 있다.
    • 따라서 테스트 코드는 프로젝트 코드에 대한 가장 정확한 문서가 된다.
    • 테스트 코드만 보면, 코드를 돌리는데 필요한 것들을 알 수 있기 때문이다.
    • 이런 맥락에서, 누군가 개발한 코드를 볼 때 테스트 코드를 먼저 보면 로직을 파악하는 데 도움이 많이 된다.
    • 테스트 코드는 이렇게 다른 개발자들을 위한 일종의 배려이기도 하다.
  • 테스트 코드는 리팩토링과 지속적인 개발을 위해 필수적이다.
    • 테스트 코드 없이 개발을 계속해서 해나가면, 추가로 개발한 코드가 기존 코드의 어떤 사이드 이펙트를 불러일으키는지 확인하기 어렵다.
    • 테스트 코드를 만들어두면, 추가로 코드를 개발할 때마다, 기존 테스트 코드를 모두 실행함으로써 기존 코드의 작동 여부에 사이드 이펙트가 있는 지 빠르게 확인할 수 있다.
    • 이런 맥락에서, 테스트 코드는 일종의 안전망이다.
    • 테스트 코드 없이 개발을 계속해나가면 매번 리팩토링과 기능 개발을 할 때마다 마음을 졸이게 된다.
0%