13 Security Lab

[Python] How to use pytest & mock & fixture 본문

Computer Science/Programming

[Python] How to use pytest & mock & fixture

Maj0r Tom 2018. 9. 18. 22:20

pytest & mock & fixture

 

 

1. pytest

 

 

python 으로 개발하면서 코드 단위가 커지면 자연스레 테스트방법을 찾게 되는데 unittest와 pytest를 검색하게 된다.

 

그 중 별 이슈가 없는 한 pytest 를 선택하게 되는데..

 

 

 

쉽고 강력한 테스트 라이브러리로서 unittest(파이썬 표준라이브러리) 와 유사하지만 단순한 문법으로 코드를 비교적 단순하게 만들 수 있다

 

 

 

실행 방법은 단순히 커맨드에서 pytest를 입력하면 된다.

 

$ pytest =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: collected 1 item test_sample.py F [100%] ================================= FAILURES ================================= _______________________________ test_answer ________________________________ def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) test_sample.py:5: AssertionError ========================= 1 failed in 0.12 seconds =========================

 

기존 테스트 케이스가 있었더라도 아무 수정 없이 실행이 가능하다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
>>pip install pytest
 
# default
>> python -m pytest
 
# or
>> pytest
 
>> pytest test_example1.py
>> pytest test_example1.py::TestApps
>> pytest test_example1.py::TestApps::test_return_itself_with_value
cs

 

 

 

 

2. Mock 

 

Method를 mock 하기 위해 사용 -> 의존성이 있는 것들을 실제로 실행 시키지 말고 호출 여부, 인터페이스만 확인 하기 위해서

 

unittest.mock 은 파이썬의 테스트 라이브러리

 

python 3.3부터는 표준 라이브러리로 별도 설치 필요 X

 

python 2에서 는 설치 필요

 

>> pip install mock

 

 

Mock 의 구현 대상이 되는 것?

 

1. 시간이 오래 걸리는 것

2. 값이 변하는 것

3. 상태가 유지 되는 것 (다른 테스트에 영향을 주는 것)

4. 시스템 콜

5. 네트워크로 연결 된 것

6. 준비하기 복잡한 것

 

 

테스트와 연관 된 서버가 다운되어도 내 테스트에영향 X

 

(Rest API 를 통해서 서버와 통신을 주고 받는 스크립트를 만든다 -> 서버와 주고 받는 부분을 Mock으로 구현)

 

 

2.1 Monky Patch (mock.patch)

 

런타임에 클래스, 함수 등을 테스트 코드로 대체하는 것주로 네트워크, 시스템 표준라이브러리를 대체함으로써 실행 없이 인터페이스나 기능을 확인할 수 있다.patch하고자 하는 대상이 test 코드에 없다면 test 소스에 import를 별도로 해주어야 한다. (mock.patch.object와 비교)(함수 전체를 대체하거나, 특정 리턴 값만 지정해 줄 수 있음)

 

example 

1
2
3
4
5
6
7
from mock import patch, MagicMock
 
@patch('mymodule.SomeClass')
class MyTest(TestCase):
 
    def test_one(self, MockSomeClass):
        self.assertIs(mymodule.SomeClass, MockSomeClass)
cs

 

 

 

 

 

Example) @mock.patch

 

함수의 목적이 아닌 mock에 포커스 맞춰서 보면

아래를 보면 두가지 method를 mock up 하고 있는데 각각 그 method의 return 값을 특정 값으로 대체하고 있는 것을 볼 수 있다.

 

test_example_process_mock_patch.py (test)

 

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
import main
import mock, pytest
 
@mock.patch('threading.Timer')
@mock.patch('subprocess.Popen')
def test_run_process_case_timeout(self, mock_subproc_popen, mock_threading_timer):
    """
    프로세스 동작 확인: Time out
    프로세스가 3초 이상 delay 되는 경우, Fail 처리하고 해당 프로세스를 강제로 kill 한다.
    """
    process_mock = mock.Mock()
    attrs_proc = {'wait.return_value''returncode''return_value': subprocess.Popen}
    process_mock.configure_mock(**attrs_proc)
    mock_subproc_popen.return_value = process_mock
 
    timer_mock = mock.Mock()
    attrs_timer = {'isAlive.return_value': False}
    timer_mock.configure_mock(**attrs_timer)
    mock_threading_timer.return_value = timer_mock
    
    test_target_file = "./test_dir/test_target"
    run_process(test_target_file)
 
    assert result_ts_proc is False
    assert mock_subproc_popen.called
    assert mock_threading_timer.return_value.isAlive.called
cs

 

 

Main.py 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
def run_process(self, input_path):
    """
    subprocess 를 통해 프로세스를 실행 시킨다.
    :param input_path: 실행 시킬 프로세스 경로
    :return: 성공 여부 (True or False)
    """
    proc = subprocess.Popen(["/bin/sh""./" + input_path],
                            shell=False, cwd=self.working_dir)
 
    # 3초 타이머 이후 프로세스가 여전히 실행 중이면 강제 종료 한다.
    t = threading.Timer(3, self.__kill_process, [proc, input_path])
 
    t.start()
    proc.wait()
 
    ret_flag = t.isAlive()  # If timer is done, it return False
    t.cancel()
    t.join()
 
    return ret_flag
 
...
cs

 

 

아래와 같이 @mock.patch로 세팅한 값은 1,2 -> 2,1 로 매칭 되므로 파라미터 전달에 유의한다.

mock.patch로 패치 된 함수는 각각 mock_subproc_popen, mock_threading_timer으로 값을 조정 할 수 있다.

 

 

 

 

 

2.2 mock.patch.object

 

mock.patch 와 달리 런타임 동안에 패치할 클래스, 함수를 확인하므로, 테스트 코드에 패치할 함수를 별도로 import 해줄 필요가 없다.

 

아래 예제는 @mock.patch.object 를 이용해서 main_process 클래스의 run_process를 mock_run_process로 아예 대체한 것 으로 파라미터로 new="mock function name" 을 전달 하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import mock, pytest, main_process
 
def mock_run_process(input_path):
    """
    [mock function] run_process 의 기능을 대체한다.
    Covered file name list: ["aaaa", "bbbb", "cccc", "dddd"]
    :param input_path: input
    :return: 성공 여부 (True or False)
    """
    input_file_name = os.path.basename(input_path)  # input file name. ex) "aaaa"
    target_test_files = ["aaaa""bbbb""cccc""dddd"]
    if input_file_name not in target_test_files:
        return False
    
    return True
 
 
@mock.patch.object(main_process'run_process', new=mock_run_process)
def test_run_process_case_success(self): 
    test_target_file = "./test_dir/test_target"
    ret_proc = main_process.run_process(test_target_file)
    assert ret_proc is True
 
cs

 

 

 

 

2.3 mock.patch vs. mock.patch.object 

 

 

공식 문서를 찾아본다.https://docs.python.org/3/library/unittest.mock.html

mock.patch 

 

 

mock.patch.object

 

 

 

???

패치를 하겠다는 것은 알겠다.

 

 

 

구글링 해보면 stackoverflow 에 좀 더 친절한 답변이 나온다.

 

 

mock.patch() takes a string which will be resolved to an object when applying the patch.

mock.patch.object() takes a direct reference.

This means that mock.patch() doesn't require that you import the object before patching, while mock.patch.object() does require that you import before patching.

The latter is then easier to use if you already have a reference to the object.

example)

mock.patch.object

1
2
3
4
5
6
7
from passlib.context import CryptContext
from unittest import mock
 
with mock.patch.object(CryptContext, 'verify', return_value=True) as foo1:
    mycc = CryptContext(schemes='bcrypt_sha256')
    mypass = mycc.encrypt('test')
    assert mycc.verify('tesssst', mypass)

 

 

 

mock.patch

 

1
2
3
4
5
6
from unittest import mock
 
with mock.patch('passlib.context.CryptContext.verify', return_value=True) as foo2:
    mycc = CryptContext(schemes='bcrypt_sha256')
    mypass = mycc.encrypt('test')
    assert mycc.verify('tesssst', mypass)

 

 

 

예제를 보면 알 수 있듯이 사용하는데는 큰 차이가 없다.

다만, 직접 선언 후 레퍼런스를 패치하는 것(mock.patch.object)과 런타임 때 생성 된 object를 패치(mock.patch)하는 데 차이가 있음을 알 수 있다.

 

 

3. Fixture

fixture란 테스팅에서 쓰이는 값이나 리소스에 대한 부분으로 미리 준비해두는 준비 도구 및 재료를 의미한다.

 

fixture 함수들의 경우 conftest.py 파일에 작성하는 것이 가이드 되고 있다.

>>> contest.py: sharing fixture functions

 

pytest fixture 등을 이용해서 아래와 같은 다양한 context를 제공하고 있다.

1) setup and teardown for each test 

2) setup and teardown for whole test 

 

https://docs.pytest.org/en/latest/fixture.html

 

Scope

Pytest의 fixture에는 다음과 같은 4가지 scope이 있고, 각각의 scope마다 한 번 씩 실행된다.

 

session: Pytest를 한 번 실행할 때마다 한 번

module: 테스트 스크립트의 모듈마다 한 번

class: 테스트 클래스마다 한 번

function: 테스트 케이스마다 한 번

 

Example)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# setup/teardown each
@pytest.fixture(scope="function", autouse=True)
def dir_setup():
    """
        setup:    기본 테스트 디렉토리 세팅
        teardown: 결과 및 디렉토리를 삭제한다.
    """
    print("setup dir_setup")
    test_dir = os.path.dirname(os.path.realpath(__file__))  # ~/tests dir
    
    test_out_dir = os.path.join(test_dir, "resources""result"
 
    if not os.path.exists(test_out_dir):
        os.mkdir(test_out_dir)
    yield dir_setup
 
    print("teardown dir_setup")
    shutil.rmtree(test_out_dir)
cs

 

위와 같이 해두면 함수의 시작과 끝에서 한번씩 호출 되며, setup과 teardown의 기능을 수행한다.

(보통 setup과 teardown을 나눠서 작성 할 수도 있지만, 아래 코드에서는 yield를 사용해서 한 함수에서 두번(시작, 끝) 호출 될 수 있도록 하였다)

 

 

 

 

다시 처음 으로 돌아와서, 어떤 테스트를 할 것인가? 

 

 

 

 

 

 

레퍼런스

https://www.slideshare.net/hosunglee948/python-52222334

Comments