파이썬 Tox



Tox는 파이썬의 테스트 자동화, 표준화를 위한 도구다. Tox를 사용하면,

  • 다양한 파이썬 버전에서 패키지가 제대로 설치되는지 확인
  • 선택한 테스트 툴과 설정을 사용해서 자동으로 테스트 실행
  • CI와 통합

이런 것들을 할 수 있다.

여기서는 Tox에서 pytest 실행 방법을 정리한다. pytest 확장인 pytest-cov, pytest-flake8도 사용해본다.


hello_flask.py

먼저, virtualenv로 dev_tox 환경을 만들고, hello_flask.py 플라스크 소스를 추가했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# hello_flask.py
from flask import Flask, jsonify

app = Flask(__name__)


@app.route('/api')
def my_microservice():
    return jsonify({'Hello': 'World!'})


if __name__ == '__main__':
    app.run()

/api 엔드포인트를 호출하면, json으로 Hello World를 반환하는 심플한 서버다.

$ curl localhost:5000/api 
{                         
  "Hello": "World!"       
}                         

test_hello_flask.py

이제, hello_flask를 테스트 하기 위한 모듈, test_hello_flask.py를 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# -*- coding: utf-8 -*-
# test_hello_flask.py
import unittest
import json
import sys
from hello_flask import app as _app


class TestApp(unittest.TestCase):
    def test_help(self):

        app = _app.test_client()
        hello = app.get('/api')

        if (sys.version_info > (3, 0)):
            body = json.loads(str(hello.data, 'utf8'))
        else:
            body = json.loads(str(hello.data).encode("utf8"))

        self.assertEqual(body['Hello'], 'World!')


if __name__ == '__main__':
    unittest.main()

15 ~ 열여덟 라인은 2.X과 3.X 버전의 str() 함수 원형이 다르기 때문에 추가했다. (아래에서 Tox로 테스트할 때, 2.7과 3.6 버전을 사용한다.)

pytest로 테스트가 성공하는지 먼저 확인해 보자.

(dev_tox) $ pytest
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: D:\Code\python_dev\dev_tox\flask, inifile:
plugins: flake8-0.9.1, cov-2.5.1
collected 1 item

test_functional_01.py .                                                  [100%]

========================== 1 passed in 0.22 seconds ===========================

Tox

이제 Tox를 사용할 차례다. pip install tox로 패키지를 설치하자.
Tox를 실행하려면, tox.ini와 setup.py가 필요하다. setup.py의 내용은, 여기서 중요하지 않으므로, 아래처럼 최소한으로 했다.

1
2
3
4
5
from setuptools import setup


setup(name='hello_flask',
      version='0.1')

tox.ini에는 테스트를 실행할 파이썬 버전, 의존성, 테스트 명령 등을 적어둔다.

[tox]
envlist=py27,py36
[testenv]
deps=flask
     pytest
commands=pytest

위와 같이, envlist에 파이썬 버전, deps에 의존성, commands에 테스트 명령을 적어준다. envlist에 기술한 파이썬 버전 2.7과 3.6은 시스템에 미리 설치해둬야 한다.

작성이 완료됐으면 tox 명령으로 실행한다.

(dev_tox) $ tox
GLOB sdist-make: D:\Code\python_dev\dev_tox\flask\setup.py
py27 recreate: D:\Code\python_dev\dev_tox\flask\.tox\py27
py27 installdeps: flask, pytest
py27 inst: D:\Code\python_dev\dev_tox\flask\.tox\dist\hello_flask-0.1.zip
py27 installed: attrs==17.4.0,click==6.7,colorama==0.3.9,Flask==0.12.2,funcsigs==1.0.2,hello-flask==0.1,
itsdangerous==0.24,Jinja2==2.10,MarkupSafe==1.0,pluggy==0.6.0,py==1.5.2,pytest==3.3.2,six==1.11.0,Werkze
ug==0.14.1
py27 runtests: PYTHONHASHSEED='435'
py27 runtests: commands[0] | pytest
============================= test session starts =============================
platform win32 -- Python 2.7.14, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: D:\Code\python_dev\dev_tox\flask, inifile:
collected 1 item

test_functional_01.py .                                                  [100%]

========================== 1 passed in 0.16 seconds ===========================
py36 recreate: D:\Code\python_dev\dev_tox\flask\.tox\py36
py36 installdeps: flask, pytest
py36 inst: D:\Code\python_dev\dev_tox\flask\.tox\dist\hello_flask-0.1.zip
py36 installed: attrs==17.4.0,click==6.7,colorama==0.3.9,Flask==0.12.2,hello-flask==0.1,itsdangerous==0.
24,Jinja2==2.10,MarkupSafe==1.0,pluggy==0.6.0,py==1.5.2,pytest==3.3.2,six==1.11.0,Werkzeug==0.14.1
py36 runtests: PYTHONHASHSEED='435'
py36 runtests: commands[0] | pytest
============================= test session starts =============================
platform win32 -- Python 3.6.3, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
rootdir: D:\Code\python_dev\dev_tox\flask, inifile:
collected 1 item

test_functional_01.py .                                                  [100%]

========================== 1 passed in 0.24 seconds ===========================
___________________________________ summary ___________________________________
  py27: commands succeeded
  py36: commands succeeded
  congratulations :)

D:\Code\python_dev\dev_tox\flask
(dev_tox) $

결과와 같이, 각 파이썬 버전 별 환경에 의존성을 설치하고 배포 파일을 생성한 다음, 테스트를 실행해준다.

pytest-cov, pytest-flake8

이번에는 테스트에 pytest-cov, pytest-flake8도 추가해 보자.

  • pytest-cov: pytest의 coverage 플러그인. 코드 커버리지 검사.
  • pytest-flake8: pytest의 flake8 플러그인. 컨벤션이 PEP8을 준수하는지 검사.

tox.ini의 의존성과 명령에 해당 도구를 추가한다.

[tox]
envlist=py27,py36
[testenv]
deps=flask
    pytest
    pytest-cov
    pytest-flake8
commands=pytest -v --cov --flake8

커버리지 측정과 컨벤션 검사가 추가된 테스트 결과를 볼 수 있다.

(dev_tox) $ tox
...
hello_flask.py SKIPPED                                                   [ 25%]
setup.py SKIPPED                                                         [ 50%]
test_hello_flask.py PASSED                                               [ 75%]
test_hello_flask.py::TestApp::test_help PASSED                           [100%]

---------- coverage: platform win32, python 2.7.14-final-0 -----------
Name                  Stmts   Miss  Cover
-----------------------------------------
hello_flask.py            6      1    83%
test_hello_flask.py      15      3    80%
-----------------------------------------
TOTAL                    21      4    81%


===================== 2 passed, 2 skipped in 0.32 seconds =====================
...

hello_flask.py SKIPPED                                                   [ 25%]
setup.py SKIPPED                                                         [ 50%]
test_hello_flask.py SKIPPED                                              [ 75%]
test_hello_flask.py::TestApp::test_help PASSED                           [100%]

----------- coverage: platform win32, python 3.6.3-final-0 -----------
Name                  Stmts   Miss  Cover
-----------------------------------------
hello_flask.py            6      1    83%
test_hello_flask.py      15      2    87%
-----------------------------------------
TOTAL                    21      3    86%


===================== 1 passed, 3 skipped in 0.33 seconds =====================
___________________________________ summary ___________________________________
  py27: commands succeeded
  py36: commands succeeded
  congratulations :)

참고
pytest-flake8은 디폴트 로그 레벨이 DEBUG라서 warning 하나만 발생해도 어마무시한 로그가 출력된다. 뭔가 이상하다 싶으면, flake8 명령만 실행해서 warning을 먼저 해결한 다음,pytest-flake8을 실행하는게 좋다. 디폴트 로그 레벨을 WARN으로 설정한 PR이 5일전에 올라왔으니 조만간 해결되지 싶다.