| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- data distribution
- idb2pat
- Analysis
- mock.patch
- hex-rays
- error fix
- Python
- Injection
- why error
- MySQL
- javascript
- open office xml
- TensorFlow
- Ransomware
- debugging
- Rat
- pytest
- ida
- x64
- svn update
- idapython
- 포인터 매핑
- error
- idapro
- ida pro
- NumPy Unicode Error
- ecma
- commandline
- h5py.File
- malware
- Today
- Total
13 Security Lab
[Python] How to use pytest & mock & fixture 본문
[Python] How to use pytest & mock & fixture
Maj0r Tom 2018. 9. 18. 22:20pytest & mock & fixture
1. pytest
python 으로 개발하면서 코드 단위가 커지면 자연스레 테스트방법을 찾게 되는데 unittest와 pytest를 검색하게 된다.
그 중 별 이슈가 없는 한 pytest 를 선택하게 되는데..
쉽고 강력한 테스트 라이브러리로서 unittest(파이썬 표준라이브러리) 와 유사하지만 단순한 문법으로 코드를 비교적 단순하게 만들 수 있다
실행 방법은 단순히 커맨드에서 pytest를 입력하면 된다.
기존 테스트 케이스가 있었더라도 아무 수정 없이 실행이 가능하다.
|
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, whilemock.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