카일 스쿨 9회차

Hits

  • #1. Test Code
  • #2. Test Code 맛보기
  • #3. 본격 Test Code 작성하기
  • Class Test
  • Fixture
  • 활용 사례
  • 최종 실습

1. Test Code

오늘 이것만은 꼭!

  • 코드를 테스트하는 방법에 대한 감을 익힌다
  • assert 키워드를 알아간다
  • pytest를 직접 터미널에서 실행해본다
  • Fixture란 무엇인지 이해한다
  • 라이브러리에서 Test Code를 확인하는 방법을 이해한다

테스트 자동화의 중요성

  • 시스템 테스트에서 가장 중요한 것은 테스트의 자동화
    • 사람이 직접 실행하는 매뉴얼 테스트만 거칠 경우, 사이드 이펙트가 생길 수 있음
    • 예시 : 함수 작성 => print나 로그로 작성하는거 => 기본적인 테스트지만, 실수할 수 있음
    • 나 대신 코드를 테스트해줄 엄격한 친구가 필요
  • 테스트를 최대한 자동화해서 테스트가 반복적으로, 자주 실행될 수 있도록 해야하며 항상 정확하게, 빠지는 부분이 없도록 테스트가 실행되도록 하는 것이 굉장히 중요함
  • 중요한 로직(예 : 가격)은 꼭 테스트 코드가 필수!
  • 테스트 방법
    • 1) UI test / End-to-End test
    • 2) integration test
    • 3) unit test

UI test / End-To-End test

  • UI Test는 시스템의 UI(User Interface)를 통해서 테스트하는 것
  • 웹이라면 웹 브라우저를 통해 웹사이트에 접속하고, UI에 직접 입력하고 클릭하는 등을 통해 기능이 정상적으로 작동하는지 테스트
  • 장점
    • 사용자가 실제로 시스템을 사용하는 방시과 가장 동일하게 테스트
  • 단점
    • 시간이 가장 많이 소요되는 테스트
    • 프론트엔드 ~ 백엔드까지 모든 시스템을 실행시키고 연결해야 함
    • 자동화하기 가장 까다로움
  • Selenium 같은 UI Test 프레임워크를 사용해 어느정도 자동화가 가능하지만 100% 자동화는 어려움
    • 특히 화면 렌더링에서 문제가 발생
  • 단점 때문에 전체 테스트 중 대략 10% 정도만 UI test 방식을 통해 실행하는 것을 추천 ( 주로 마지막에 테스트 )

Integration test

  • API 서버를 실행시키고 HTTP Request를 실행해 Response가 올바른지 파악
  • 테스트하고자 하는 서버를 실제로 실행시키고 테스트 HTTP 요청을 실행해 테스트해보는 방식
  • 하나의 시스템만 실행해서 UI test에 비해 테스트 설정이나 실행 시간이 더 짧고 간단
  • 하지만 unit test에 비해 자동화에 걸리는 공수가 더 크고 실행 속도도 더 느릴 수 밖에 없음
  • 정리하면, 최소 기능 단위를 묶어서 테스트함
  • 전체 테스트 중 대략 20% 정도만 할당하는 것을 추천

Unit test

  • 시스템을 테스트한다는 개념보다는 코드를 직접 테스트하는 개념
    • 즉, 코드로 코드를 테스트함
  • 실행하기 쉬우며 실행 속도도 빠름
    • 디버깅도 비교적 쉬움
    • 함수 단위로 테스트해서 파악이 쉬울 수밖에 없음
  • 단점은 함수 단위로 테스트하다보니 전체적인 부분을 테스트하기엔 제한적일 수 밖에 없음
    • 이런 단점을 integration test와 UI test를 통해 보완
  • 전체 테스트의 70%를 unit test

본격 시작하기 전에

  • Assert를 알아야 합니다
  • 가정 설정문을 뜻하는 assertion에서 나옴
    • 개발 과정에서 실수가 일어난다 가정하고 특정 시점에 가정문을 작성함
  • 프로그래밍에서 코드를 점검할 때 사용함
  • 특정 지점에서 항상 참이어야 하는 문장
  • 문법
assert condition
  • 이 condition이 맞지 않으면 Error를 발생!
In [36]:
assert 1
assert 1==1
assert 1==2
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-36-529b3dba3ebd> in <module>
      1 assert 1
      2 assert 1==1
----> 3 assert 1==2

AssertionError: 
In [35]:
assert 1==2, "이건 참이 아니지요"
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-35-a4db5263a64b> in <module>
----> 1 assert 1==2, "이건 참이 아니지요"

AssertionError: 이건 참이 아니지요
In [39]:
int_value = 3
assert type(int_value) == int
assert type(int_value) == str
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-39-6b45681bf9aa> in <module>
      1 int_value = 3
----> 2 assert type(int_value) == str

AssertionError: 

2. Test Code 맛보기

  • 주로 사용하는 라이브러리
  • 파이썬 기본 내장인 unittest와 pytest 등이 주로 사용됨
  • 이번 시간엔 pytest를 사용할 예정

폴더 구조

├── tests : Test Code가 저장되는 폴더
│   ├── __init__.py
│   └── test_your_module.py
└── your_module.py : 작성한 파일

pytest

  • 파이썬 내장 라이브러리인 unittest보다 사용하기 직관적인 pytest를 추천함

    • unittest는 단순 assert문이 아니라, self.assertEqual 같은 함수를 사용해야 하고, 항상 클래스로 만들어야 하는 반면, pytest는 assert를 사용하고 클래스가 아닌 함수도 사용 가능
  • 설치

      pip3 install pytest
  • Test하는 방식

    • 함수 생성 : function_name.py
    • 그 함수를 테스트하는 테스트 케이스 생성 : test 폴더 아래에 test_function_name.py로 작성
    • 테이스 케이스 실행 : 터미널에서 pytest 실행
  • 자 간단한 것부터 해봅시다

    • 처음이니 노트북에서 진행할게요
    • %%writefile -a 파일명 : 파일에 append(아래에 추가 입력)하며 저장
In [24]:
%%writefile -a utils.py
# 이 파일을 실행하면 utils.py에 파일이 저장됩니다
import datetime

def is_working_day(date: datetime.date):
    """
    date를 받아서 근무일인지 확인하는 함수
    연휴는 고려하지 않고, 토/일은 근무일이 아니고 월~금은 근무일
    """
    weekday = date.weekday()
    if weekday in {5, 6}:
        return False
    else:
        return True
Appending to utils.py
In [25]:
%%writefile test_utils.py
# test_utils.py를 아래 내용으로 저장합니다

from utils import is_working_day

def test_is_working_day():
    assert is_working_day(datetime.date(2020,7,5)) == False
    assert is_working_day(datetime.date(2020,7,4)) == False
    assert is_working_day(datetime.date(2020,7,6)) == True
Overwriting test_utils.py
In [26]:
!pytest test_utils.py
# pytest를 실행합니다!
Test session starts (platform: darwin, Python 3.7.4, pytest 5.0.1, pytest-sugar 0.9.2)
rootdir: /Users/byeon/Dropbox/workspace/kyle-school/notebooks
plugins: sugar-0.9.2, dash-1.0.0, mock-1.10.4
collecting ... 

――――――――――――――――――――――――――――― test_is_working_day ――――――――――――――――――――――――――――――

    def test_is_working_day():
>       assert is_working_day(datetime.date(2020,7,5)) == False
E       NameError: name 'datetime' is not defined

test_utils.py:6: NameError

 test_utils.py                                                  100% █████████

Results (0.20s):
       1 failed
         - test_utils.py:5 test_is_working_day
  • NameError: name 'datetime' is not defined
  • 오류가 날 경우 이렇게 알려줍니다
  • 그럼 고쳐봅시다
In [27]:
%%writefile test_utils.py
# test_utils.py를 아래 내용으로 overwrite합니다(-a 옵션 없이!)
import datetime
from utils import is_working_day

def test_is_working_day():
    assert is_working_day(datetime.date(2020,7,5)) == False
    assert is_working_day(datetime.date(2020,7,4)) == False
    assert is_working_day(datetime.date(2020,7,6)) == True
Overwriting test_utils.py
In [28]:
!pytest test_utils.py
Test session starts (platform: darwin, Python 3.7.4, pytest 5.0.1, pytest-sugar 0.9.2)
rootdir: /Users/byeon/Dropbox/workspace/kyle-school/notebooks
plugins: sugar-0.9.2, dash-1.0.0, mock-1.10.4
collecting ... 
 test_utils.py                                                  100% █████████

Results (0.03s):
       1 passed
  • Good! 성공
  • pytest에선 test_라고 되어있는 파일들만 테스트 파일로 인식하고, 함수도 test_라고 prefix가 있어야만 test할 함수로 인식함

3. 본격 Test Code 작성하기

  • 이번엔 스크립트로 작성합시다
  • VS Code나 Pycharm을 켜주세요(아무 폴더)
  • your_module.py를 만듭시다

      def multiply_by_two(x):
          return x * 2
  • tests/test_your_module.py도 만듭시다

      from your_module import *
    
      def test_multiply_by_two():
          assert multiply_by_two(2) == 4
          assert multiply_by_two(3.6) == 7.2
  • tests/__init__.py 빈 파일을 만들어주세요

  • pytest 실행

    • 소스 코드에서 다음을 실행합니다

      pytest tests/
    • 성공!

  • 직접 따라쳐봅시다

  • 이번엔 tests/test_your_module.py에 다음과 같이 입력해볼게요(일부러 오류 발생)

    • 2가지 모두 실패!

      from your_module import *
      
      def test_multiply_by_two():
        assert multiply_by_two(2) == 5
        assert multiply_by_two(3.6) == 7.9
  • 실패!
    • 위에 테스트가 실패되서 아래꺼는 실행되지 않음
  • 하나의 함수만 지정해서 테스트하기

    • 파일 지정하고 :: 뒤에 함수 작성

      pytest tests/test_your_module.py::test_multiply_by_two
  • 키워드 지정해서 테스트하기

    • -k 에 단어 지정

      pytest tests/test_your_module.py -k multiply
  • Error가 발생하는지 Test하기

      def test_divide_by_zero():
          with pytest.raises(ZeroDivisionError) as e:
              3/0

이번엔 Class Test

  • simple_class.py를 만듭시다
  • 간단한 Queue입니다
class Queue:
    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def first(self):
        return self.items[0]

    def last(self):
        return self.items[-1]

    def length(self):
        return len(self.items)
  • 이제 test/test_simple_class.py를 만들어봅시다
from simple_class import Queue

def test_firstlast():
    q = Queue()

    q.add_item(5)
    q.add_item(17)
    q.add_item("hello")

    assert q.first() == 5
    assert q.last() == "hello"

def test_len():
    q = Queue()

    assert q.length() == 0

    q.add_item(1)

    assert q.length() == 1

    for i in range(10):
        q.add_item(i)

    assert q.length() == 10
  • pytest 실행
pytest test/
  • 생각해볼 점
    • 각 테스트는 다른 테스트에 독립적이어야 함
    • 여러 테스트를 할 때, 공통적으로 사용하는 리소스가 있을 수 있음
      • 예 : 하나의 인스턴스를 공통으로 사용해야 하는 경우
    • Don't Repeat Yourself(DRY)에 따라 매번 테스트할 때 반복하면 X
      • simple_class.py 예제에서 Queue는 생성할 떄 얼마 걸리지 않았지만, 인스턴스 생성시 시간이 오래 걸리거나 or 큰 csv 파일을 읽어야 하는 경우엔?
      • read_csv() 함수 등으로 시간이 오래 걸림
    • 이럴 때 사용하는 것이 Fixture

Fixture

  • 테스트에 필요한 공통 리소스(자원)
  • 함수 형태로 작성해서 활용함
  • 테스트를 할 때 필요한 부분이나 조건들을 미리 준비한 리소스
  • Test Case에서 필요한 fixture를 쉽게 사용
    • 특정 상황
      • 예 : 월요일 아침에 수요가 100개고, 비가 오는 경우엔 어떤 값이 반환?
      • 예 : 월요일 아침에 수요가 100개고, 비가 안오는 경우엔 어떤 값이 반환?
    • 데이터베이스에 접근해야 하는 경우(계정 정보)
    • 딥러닝 모델 객체
    • Selenium 웹드라이버
    • csv 파일을 읽은 Dataframe
  • Dependency Injection이라 표현(의존성 주입)
  • 테스트의 독립 + 리소스 재사용을 할 수 있게 해주는 방법
  • Fixtures 사용하기
    • @pytest.fixture 데코레이터를 사용
    • 테스트할 다른 함수에서 함수명을 객체처럼 사용할 수 있음
  • 문법

      @pytest.fixture(scope="fixture_scope")
      def my_fixture():
          return fixture_object
    
      def test_sample_function(my_fixture):
          # test code
  • utils.py

      import pandas as pd
    
      def load_data():
          df = pd.read_csv("iris.csv")
          return df
  • tests/test_utils.py

      import pytest
      import pandas as pd
      from utils import load_data
    
      @pytest.fixture(scope="session")
      def result_fixture():
          result = load_data()
          return result
    
      def test_len(result_fixture):
          assert len(result_fixture) == 150
    
      def test_object_type(result_fixture):
          assert isinstance(result_fixture, pd.DataFrame)
  • @pytest.mark.parametrize()

    • 파라미터로 넘겨서 여러가지를 한꺼번에 테스트할 경우 사용

      import pytest
      
      @pytest.mark.parametrize("test_input, expected", 
                            [("3+5", 8), 
                             ("2+4", 6), 
                             ("6*9", 42)])
      
      def test_eval(test_input, expected):
        assert eval(test_input) == expected

활용 사례

  • Pandas에서 Test Code는 어떻게 되어있을까?
  • 딥러닝 프로젝트에서 Test Code는 어떻게 되어있을까?

최종 실습

  • Calculator Test Code 만들기
  • cals_func.py 파일
def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def multiply(a, b):
    return a * b


def divide(a, b):
    # automatically raises ZeroDivisionError
    return a * 1.0 / b


def maximum(a, b):
    return a if a >= b else b


def minimum(a, b):
    return a if a <= b else b
  • cals_class.py 파일이 있습니다
from calc_func import *

class Calculator(object):
    def __init__(self):
        self._last_answer = 0.0

    @property
    def last_answer(self):
        return self._last_answer

    def _do_math(self, a, b, func):
        self._last_answer = func(a, b)
        return self.last_answer

    def add(self, a, b):
        return self._do_math(a, b, add)

    def subtract(self, a, b):
        return self._do_math(a, b, subtract)

    def multiply(self, a, b):
        return self._do_math(a, b, multiply)

    def divide(self, a, b):
        return self._do_math(a, b, divide)

    def maximum(self, a, b):
        return self._do_math(a, b, maximum)

    def minimum(self, a, b):
        return self._do_math(a, b, minimum)
  • test_calc_class.py
    • 여기 나와있는 TODO 채우기^_^
    • 다음 카일스쿨까지 숙제