포스트

Python의 모듈테스트 Unittest

파이썬에서 java JUnit을 본딴 유닛 테스트 도구인 unittest를 알아보자

(출처 docs )

Unittest가 필요한 이유

  • 자바를 배울 때 intellij에서 spring을 다룰 때 단위 테스트를 손쉽게 진행할 수 있고 단축키를 통해 빠르게 테스트 코드를 짤 수 있다고 했었던가.
  • 파이썬 코드를 개발하면서도 물론 당연히 요구사항을 점검하는 테스트 코드는 필요하다. (잘 안하게 될 뿐..)
    • 잘 안지켜지는건 동적 타이핑 언어의 특성이라고도 생각한다.
  • 다만 CI를 위해 테스트는 꼭 필요하다.
    • github 협업을 위해서는 CI(Code Integration)를 잘 마련해 놓아야 코드 베이스 관리에 에너지를 덜 사용한다!
    • 여러 케이스에 대한 테스트 코드가 잘 준비된 레포지토리는 어떤 랜덤 유저의 pull request에도 강경하다. (이게 좋은 오픈소스 프로젝트를 판가름하는 요소같기도 하다.)

Unittest의 사용 방법과 주요 시나리오

테스트를 하는 이유

  • 클라이언트의 요구사항 점검 ex. 영문자는 항상 대문자로 나오게 해주세요.
  • 만든 코드가 예상 시나리오들에서 잘 작동하는지 점검
  • 프로젝트가 확장되고 커짐에 따라 이식성 및 기존 코드의 안정성을 테스트 코드로 보장하기 위함 (개인적 생각..)

unittest 사용 방법

  • 기본적인 틀은 다음과 같다.
    • 시험하고자 하는 모듈 load
    • 모듈 내 혹은 바깥에 외부 변수 혹은 외부 모듈이 있다면 상수로 고정하거나 Mock을 통해 고정한다.
    • input/output을 정의하고 예상하는 결과 predict와 비교하는 assert 문 제작
  • 예시 코드
    • 테스트 대상 class method (RandomAlphabet.get_upper_alpha_string)
      • 길이 n의 대문자 문자열을 반환한다.
    • 테스트 클래스 (TestAlpha)
      • target 길이 n을 주고 반환받은 문자열이 대문자인지 검사한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# target module
import random  
import string  
  
  
class RandomAlphabet:  
    @staticmethod  
    def select_random_alpha():  
        letters = string.ascii_letters  
        return random.choice(letters)  
  
    @staticmethod  
    def to_uppercase(input_string: str):  
        return input_string.upper()  
  
    def get_upper_alpha_string(self, n: int):  
        result_str = ""  
        for _ in range(n):  
            result_str += self.select_random_alpha()  
  
        return self.to_uppercase(result_str)  
  
  
if __name__ == "__main__":  
    ra = RandomAlphabet()  
  
    res = ra.get_upper_alpha_string(5)  
    print(res)
  • 테스트 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import unittest  
  
from rand_alpha_module import RandomAlphabet  
  
  
class TestAlpha(unittest.TestCase):  
    @classmethod  
    def setUpClass(cls):  
        cls.rand_alpha_instance = RandomAlphabet()  
  
    def test_upper(self):  
        target_length = 10  
  
        upper_alpha_string = self.rand_alpha_instance.get_upper_alpha_string(  
            n=target_length  
        )  
  
        print(f"given upper_alpha_string is {upper_alpha_string}")  
  
        assert upper_alpha_string == upper_alpha_string.upper(), "given string is not perfect upper string."  
  
  
if __name__ == "__main__":  
    unittest.main()

# output
given upper_alpha_string is OHZTHTDMLI
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

시나리오1: 클라이언트 요구사항 점검

  • 모듈을 설계하면서 수행해야하는 요구사항들을 제대로 수행하는지 테스트한다.
  • 예시 - 길이 5의 경우가 가장 많으니 길이가 5인 10문장을 추출하는 데 문제가 없어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def test_10_upper(self):  
    target_length, num_iter = 5, 10  
  
    for i in range(num_iter):  
        upper_alpha_string = self.rand_alpha_instance.get_upper_alpha_string(  
            n=target_length  
        )  
        print(f"idx:{i} {upper_alpha_string}")  
  
        assert upper_alpha_string == upper_alpha_string.upper(), "given string is not perfect upper string."

# output
idx:0 CPWEA
idx:1 WHSMP
idx:2 BHBQJ
idx:3 TUNDD
idx:4 AXSQR
idx:5 WBSLX
idx:6 CLXHK
idx:7 NPFOL
idx:8 JYVAD
idx:9 TWYBC
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

시나리오2: 예상 시나리오들에서의 동작 점검

  • 특정 연속성이 있는 흐름에서 제대로 작동하는지를 점검한다.
    • A 모듈 -> B 모듈 -> C 모듈을 각각 테스트하고 일련의 흐름 또한 테스트하고 싶은 경우 (이 예시에서는 중간 모듈인 select_random_alpha를 고정해보자)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def test_fixed_alpha_string(self):  
    target_length = 5  
    target_alpha_string = "AAAAA"  
  
    # RandomAlphabet 클래스의 'select_random_alpha' method를 Mocking하여 항상 알파벳 'a'를 리턴하게 만든다.  
    self.rand_alpha_instance.select_random_alpha = Mock()  
    self.rand_alpha_instance.select_random_alpha.return_value = "a"  
  
    upper_alpha_string = self.rand_alpha_instance.get_upper_alpha_string(  
        n=target_length  
    )  
    print(f"given upper_alpha_string is {upper_alpha_string}")  
  
    assert upper_alpha_string == target_alpha_string, "given string is not perfect upper string."

# output
given upper_alpha_string is AAAAA
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

시나리오3: 확장 대비 이식성을 위한 성능 보존/기록용

  • 어느 코드를 재활용할 때 이 코드가 보장해야 하는 활용처가 있을 것이다. 이 테스트 코드들을 보고, 또 돌려보며 내가 활용하고자 하는 코드인지 혹여 기능이 모자라지는 않을지 추가해야할지 등을 가늠할 수 있다.

github의 CI 도구로 unittest를 활용하는 법

git action 활용

  • git action을 통해 test code들을 실행하는 shell script를 짤 수 있다.
  • 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: ci-python-unittest

on:
  pull_request:
    branches: [dev, master]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.7]
    env:
      AWS_ACCESS_KEY_ID: $
      AWS_SECRET_ACCESS_KEY: $
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python $
        uses: actions/setup-python@v1
        with:
          python-version: $
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r ./request_handler/requirements.txt
          pip install boto3
      - name: Test with unittest
        run: |
          cd request_handler
          python -m unittest discover -s ./test  -p 'test_*.py'
# 코드 출처 https://github.com/philschmid/github-actions/blob/master/python/run-unittest-on-pr-open.yaml
  • 위 git action은 dev, master 브랜치에 pull request가 발생했을 때
  • 현재 코드 베이스 기반으로 unittest를 진행한다. -> 이를 통해 궁극적으로 코드가 병합될 때 최소한의 방어막을 구축할 수 있다.

추가 정보

  • Mock처럼 특정 모듈을 모킹하는 기능이 있을 뿐만 아니라 side_effect 옵션을 이용해 좀더 유연한 값 혹은 예외를 의도적으로 발생시킬 수 있다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.