::: IT인터넷 :::

Jenkins 파이썬 빌드 구성의 예제 코드 만들기 (2)

곰탱이푸우 2021. 12. 27. 08:20
파이썬 빌드를 위한 예제 코드를 생성하는 방법에 대해 정리한다.
기능을 정의하고 코드와 테스트를 구현하고, whl (Wheel) 패키지로 빌드하여 배포하는 과정을 다룬다.
 
  1. 기능 정의
  2. 프로젝트 생성
  3. 기능 코드 작성 (srtest)
  4. 테스트 코드 작성 (tests)
  5. 패키지 정의
  6. 테스트
  7. 형상 관리

 

이전 포스팅에서 1. 기능 정의 부터 3. 기능 코드 작성 (srtest) 부분까지 다뤘다.
해당 내용은 아래 포스팅을 참고한다.

이번 포스팅에서는 4. 테스트 코드 작성(tests)과 5. 패키지 정의 부분을 다룬다.

 

 

테스트 코드 작성 (tests)

기능 구현이 완료되면 정상적으로 동작하는지 확인하기 위한 테스트 코드를 작성한다.
구현한 기능을 테스트하는 개발 방법을 테스트 주도 개발 (TDD, Test Driven Development)이라고 한다.
 
TDD는 아래 문서에 잘 정리가 되어 있으므로 참고한다.
예제 코드에서는 main 함수의 init과 display에 대한 테스트는 작성하지 않았다.
emailclient.py에 구현한 기능에 대한 유닛 테스트만 작성했는데, 예제를 간소화하기 위한 것이 목적이다.
실제 개발 업무에서는 테스트의 마지막 단계로 main 함수의 기능을 검증하는 것을 추천한다.
 

srtest-config/config.yml

테스트 코드 부분을 보기 전에 먼저 yml 타입의 설정 파일을 볼 필요가 있다.
 
해당 설정 파일은 다음과 같은 yaml 포맷을 가진다.
기능 구현에 사용한 설정 파일과 동일하다.
 
기능 구현 부분에 있는 파일을 사용해도 되지만, 경로 지정의 번거로움과 테스트에 자유롭게 사용하기 위해 별도로 생성했다.
 
자세한 내용은 주석을 참고한다.
common:    # 최상위 key
  email:    # 상위 key
    # :를 구분자로 좌측이 key, 오른쪽이 value
    sender: "no-reply@bearpooh.com"    # 원하는 값으로 수정
    receiver: "이메일계정@gmail.com"    # 원하는 값으로 수정
    subject: "test-mail"
    date: -@year-@month-@day    # 이 부분의 값을 변경한다
 

test_emailclient.py

emailclient.py 의 테스트 코드이므로, 파일명 앞에 test_를 추가했다.
테스트 코드 작성과 실행을 위해 pytest를 사용했고 pip로 설치해야 한다.
pytest에 대한 의존성 지정은 뒤에서 다룰 test_requirements.txt에 추가한다.

테스트의 전체 코드는 다음과 같다.

더보기

test_emailclient.py

import pytest
import os
import yaml
import textwrap
from datetime import datetime
from srtest.emailclient import EmailClient
from freezegun import freeze_time


class TestEmailClient:
    @pytest.fixture(scope="function", autouse=True)
    def setup_teardown(self):
        """
        config.yml 파일을 읽고 dictionary 타입으로 저장한다.
        """
        test_folder = os.path.dirname(os.path.realpath(__file__))
        self.test_conf_file = os.path.join(test_folder, "resources", "config.yml")

        with open(self.test_conf_file, 'r', encoding='utf-8') as yaml_file:
            self.yaml_dict = yaml.load(yaml_file, Loader=yaml.Loader)

    @freeze_time(datetime(2020, 3, 25, 10, 25, 22))
    def test_set_curr_time(self):
        """
        set_curr_time을 통해 현재 시각을 제대로 기록하는지 확인한다.
        """
        test_cls = EmailClient()
        test_cls.set_curr_time()
       
        assert str(test_cls.curr_time)[0:19] == "2020-03-25 10:25:22"

    @freeze_time(datetime(2020, 3, 25, 14, 25, 22))
    def test_set_email_info(self):
        """
        config.yml에서 읽은 이메일 관련 정보가 예상대로 나오는지 확인한다.
        """
        test_cls = EmailClient()
        test_cls.set_curr_time()
        message = test_cls.set_email_info(self.test_conf_file)

        subject = message['Subject']
        expected_subject = f"test-mail"
        assert subject == expected_subject

        sender = message['From']
        expected_sender = f"no-reply@bearpooh.com"
        assert sender == expected_sender

        receiver = message['To']
        expected_receiver = f"be4rpooh02@gmail.com"
        assert receiver == expected_receiver

        mail_body = message['Body']
        expected_mail_body = textwrap.dedent(f"""
            2020-03-25에 테스트를 수행했습니다.

            From bearpooh.com auto-mailing
            """)
        assert mail_body == expected_mail_body

    def test_applying_magic_keyword(self):
        """
        yml의 year, month, date를 정상적으로 치환하는지 확인한다.
        """
        payload = self.yaml_dict['common']['email']

        test_date = datetime(2019, 7, 31, 20, 0)
        test_year = str(test_date.year)
        test_month = str(test_date.month).zfill(2)
        test_day = str(test_date.day).zfill(2)

        result_dict = EmailClient._apply_value_to_magic_keyword(dictionary=payload, \
                      year=test_year, month=test_month, day=test_day)

        expected_result_args = f"2019-07-31"

        assert result_dict['date'] == expected_result_args
 
 
import 부분
import는 해당 py 파일 내부에서 사용하는 라이브러리들을 정의한다.
자세한 설명은 주석을 참고한다.
import pytest    # 파이썬 유닛 테스트 (설치 필요)
import os    # 파이썬의 파일과 경로 처리 (파이썬 기본 제공)
import yaml    # yaml 포맷 처리 (설치 필요)
import textwrap    # 여러 행의 문자열을 처리 (파이썬 기본 제공)
from datetime import datetime    # 날짜 처리 (파이썬 기본 제공)
from freezegun import freeze_time    # 테스트를 위해 특정 시간 고정 (설치 필요)

# emailclient.py에 구현한 설정 파일 처리 (직접 구현)
from srtest.emailclient import EmailClient
 
테스트 클래스 정의
테스트를 위한 클래스를 정의하는 부분이다.
 
첫 부분에 test에 사용하기 위한 기본 조건을 설정하는 setup_teardown이라는 함수를 정의하게 되어 있다.
해당 함수에는 pytest에서 제공하는 fixture라는 데코레이터를 사용했다.
 
scope 옵션으로 해당 함수를 실행하는 단위를 설정할 수 있다.
function은 테스트 함수가 실행될때 마다 먼저 실행하고, session은 테스트 코드 실행하기 전에 한번만 실행한다.
테스트 조건에 맞게 사용하면 되고, 여기서는 기본 값인 function을 사용했다.
고정적인 값이 반복적으로 사용되는 경우에는 session을 사용하면 된다.
 
pytest.fixture 데코레이터에 대한 자세한 설명은 공식 문서를 참고한다.
작성한 코드에 대한 자세한 설명은 주석을 참고한다.
class TestEmailClient:
    @pytest.fixture(scope="function", autouse=True)
    def setup_teardown(self):
        """
        config.yml 파일을 읽고 dictionary 타입으로 저장한다.
        """
        test_folder = os.path.dirname(os.path.realpath(__file__))
        self.test_conf_file = os.path.join(test_folder, "resources", "config.yml")
        
        with open(self.test_conf_file, 'r', encoding='utf-8') as yaml_file:
            self.yaml_dict = yaml.load(yaml_file, Loader=yaml.Loader)
 
test_set_curr_time 함수
EmailClient 클래스의 set_curr_time 함수에 대한 테스트 코드이다. 함수명 앞에 test_를 추가했다.
 
freeze_time 데코레이터에 datetime 함수의 결과 값을 전달했다.
해당 데코레이터는 함수가 실행되는 동안 시간을 전달 받은 값으로 고정한다.
시간을 특정 값으로 고정하여, 전달 된 값이 정상적으로 처리 되는지 확인하는데 유용하다.
 
테스트 함수는 기능을 호출하는 부분과 결과를 확인하는 부분으로 구성된다.
자세한 설명은 주석을 참고한다.
    # 2020-03-25 10:25:22 값으로 고정
    @freeze_time(datetime(2020, 3, 25, 10, 25, 22))
    def test_set_curr_time(self):
        """
        set_curr_time을 통해 현재 시각을 제대로 기록하는지 확인한다.
        """
        test_cls = EmailClient()    # 테스트를 위해 EmailClient 클래스의 객체 생성
        test_cls.set_curr_time()    # set_current_time 함수 호출하여 현재 시간 생성
       
        # 반환된 값이 freeze_time 데코레이터로 고정한 시간과 동일한지 확인
        # 같지 않으면 assert 예외가 발생하며 테스트가 실패한다
        assert str(test_cls.curr_time)[0:19] == "2020-03-25 10:25:22"
 
 
test_set_email_info 함수
EmailClient 클래스의 test_set_email_info 함수에 대한 테스트 코드이다. 함수명 앞에 test_를 추가했다.
freeze_time 데코레이터에 datetime 함수의 결과 값을 전달하여 테스트 시간을 고정했다.
 
테스트 함수는 기능을 호출하는 부분과 결과를 확인하는 부분으로 구성된다.
set_email_info를 호출하여 반환 받은 결과가 의도한 값과 같은지 확인한다.
 
자세한 설명은 주석을 참고한다.
    # 2020-03-25 14:25:22 값으로 고정
    @freeze_time(datetime(2020, 3, 25, 14, 25, 22))
    def test_set_email_info(self):
        """
        config.yml에서 읽은 이메일 관련 정보가 예상대로 나오는지 확인한다.
        """
        # 기능을 호출하는 부분
        test_cls = EmailClient()    # 객체 생성
        test_cls.set_curr_time()    # 현재시간 생성 (데코레이터에 지정한 값 전달)
        message = test_cls.set_email_info(self.test_conf_file)    # 기능 호출하여 결과 반환
        
        # 반환 결과를 비교하는 부분
        subject = message['Subject']    # 결과에서 제목 추출
        expected_subject = f"test-mail"    # 기대한 제목 값
        assert subject == expected_subject    # 같지 않으면 테스트 실패로 assert 예외 발생
        
        sender = message['From']    # 결과에서 발신인 추출
        expected_sender = f"no-reply@bearpooh.com"    # 기대한 발신인 값
        assert sender == expected_sender    # 같지 않으면 테스트 실패로 assert 예외 발생
        
        receiver = message['To']    # 결과에서 수신인 추출
        expected_receiver = f"이메일계정@gmail.com"    # 기대한 수신인 값
        assert receiver == expected_receiver    # 같지 않으면 테스트 실패로 assert 예외 발생
        
        mail_body = message['Body']    # 결과에서 본문 추출
        # 데코레이터에서 2020-03-25로 고정했으므로 본문 날짜는 2020-03-25가 되어야 함
        expected_mail_body = textwrap.dedent(f"""
            2020-03-25에 테스트를 수행했습니다.     
            From bearpooh.com auto-mailing
            """)    # 기대한 본문 값
        assert mail_body == expected_mail_body    # 같지 않으면 테스트 실패로 assert 예외 발생
 
test_applying_magic_keyword 함수
EmailClient 클래스의 _apply_value_to_magic_keyword 함수에 대한 테스트 코드이다. 함수명 앞에 test_를 추가했다.
 
테스트 함수는 기능을 호출하는 부분과 결과를 확인하는 부분으로 구성된다.
_apply_value_to_magic_keyword 함수를 호출하여 반환 받은 결과가 의도한 값과 같은지 확인한다.
 
자세한 설명은 주석을 참고한다.
    def test_applying_magic_keyword(self):
        """
        yml의 year, month, date를 정상적으로 치환하는지 확인한다.
        """
        # 클래스의 setup_teardown 함수에서 읽은 yaml_dict 값 가져오기
        payload = self.yaml_dict['common']['email']
        
        # 테스트에 사용할 값 정의
        test_date = datetime(2019, 7, 31, 20, 0)    # 2019-07-31 20:00:00 지정
        test_year = str(test_date.year)    # 연도 값을 문자열 형태로 변환
        test_month = str(test_date.month).zfill(2)    # 월 값을 문자열 형태로 변환
        test_day = str(test_date.day).zfill(2)    # 일 값을 문자열 형태로 변환
        
        # _apply_value_to_magic_keyword 함수에 payload 값과 생성한 연도-월-값 전달
        result_dict = EmailClient._apply_value_to_magic_keyword(dictionary=payload, \
                      year=test_year, month=test_month, day=test_day)

        # result_dict의 date 부분의 -@year-@month-@day가 2019-07-31로 변경되어야 한다
        expected_result_args = f"2019-07-31"
        
        # 결과 값이 기대한 값과 다르면 assert 예외 발생
        assert result_dict['date'] == expected_result_args

 

 

패키지 정의

기능과 테스트의 코드 구현이 완료되면, 사용한 라이브러리들의 의존성을 해결해야 한다.
 
일반적으로 기능 구현에 필요한 라이브러리는 requirements.txt에, 테스트에 필요한 라이브러리는 test_requirements.txt에 작성한다.
참고로 파이썬에서 기본 제공하는 라이브러리는 제외하고, pip로 설치해야 하는 외부 라이브러리만 작성한다.

 

자세한 내용은 아래 포스팅을 참고한다.
setup.py가 구현한 패키지를 설치할 때 의존성 해소를 위해 requirements.txt에 작성한 라이브러리를 먼저 설치한다. 
또한 test_requirements.txt에 작성한 라이브러리는 테스트할 때 필요하다.
 

requirements.txt

기능 구현에서 사용한 외부 라이브러리는 yaml 포맷을 파싱하는 PyYAML이다.
기능 구현과 테스트에서 5.2 버전을 사용했으므로, 다음과 같이 5.2 버전을 지정한다.
PyYAML==5.2
 
>=를 사용해서 해당 버전 이후의 최신 버전을 사용하도록 지정할 수도 있다.
해당 버전도 유용한 방법이지만, 버전이 바뀌면서 사용한 기능의 변경 사항이 있는지 체크할 필요가 있다.
버그는 일반적으로 기능 구현이 잘못되어 발생하지만, 의존성 있는 라이브러리의 변경으로 인해 예기치 않게 발생할 수도 있다.
 

test_requirements.txt

테스트에서 사용한 외부 라이브러리는 yaml 포맷을 파싱하는 PyYAML이다.
해당 라이브러리는 기능 구현 부분에서도 사용했다.
 
테스트에서 시간을 고정하기 위해 사용한 추가 라이브러리인 freezegun도 지정해야 한다.
 
또한 파이썬의 단위 테스트를 사용하기 위해 사용한 라이브러리인 pytest와  pytest-runner도 지정해야 한다.
파이썬 내부 모듈로 unittest를 제공하지만, 사용의 편리성과 익숙함으로 인해 pytest를 사용했다.
필요에 맞게 사용하면 된다.
 
기능과 테스트 작성에 사용한 버전을 명시한다.
requirements.txt와 동일하게 이후 버전에서 변경 사항이 없으면 >= 등으로 최신 버전을 사용하도록 지정할 수도 있다.
pytest-runner==5.2
pytest==5.4.3
PyYAML==5.2
freezegun==0.3.12
 
여기까지 작성하면 모든 코드 작성은 완료된 것이다.

 

이후 포스팅에서는 6. 테스트와 7. 형상 관리에 대해 다룬다.

https://www.bearpooh.com/114