[Flask] Pytest로 테스트하기

짱구석·2020년 12월 27일
1
post-thumbnail
post-custom-banner

살다보면 이래저래 시간이 없다는 핑계로 그냥 넘어가자하는 순간들이 많다.
하지만 넘기고 나서 계속해서 신경 쓰이고 불편해서 '아...그냥 그때라도 할걸..' 하는 순간들이 생각보다 많은데 고것이 나에게는 Test다. 바쁘다..진도도 나가고 싶고 문서도 만들고 싶고 Validation 라이브러리도 적용하고 싶다. 하지만 그런 자유를 얻으려면 반드시 테스트가 필요하다. 우리는 할 수 있다. 테스트를 하자.

목표

flask로 user CR 만들기

  • 준비물
    • flask
    • mysql
    • pytest
    • sqlalchemy

하나. 시작은 항상 가상환경부터

sudo apt-get install python3-venv #설치
python3 -m venv venv #생성
. venv/bin/activate #활성화

이쯤되면 외울때도 됫겠다.

둘. requirments 설치

pip install flask
pip install sqlalchemy
pip install mysqlclient

셋. app 작성

# in main.py
from flask import Flask, jsonify, request
from flask.views import MethodView

from sqlalchemy    import create_engine, text

import config

def create_app(test_config=None):
    app = Flask(__name__)

    if test_config:
        app.config.update(test_config)
    database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0)

    # 뷰
    class UserListView(MethodView):
        def __init__(self, database):
            self.db = database

        def get(self):
            users = self.db.execute("SELECT * FROM users")
            return jsonify({'users': [dict(row) for row in users] }), 200

    # 라우팅
    app.add_url_rule('/user', view_func=UserListView.as_view('user',db))
    return app

if __name__ == '__main__':
    app = create_app()
    app.run(host='0.0.0.0', debug=True, port=8000)

간단하게 sqlachemy를 이용해서 유저정보를 작성하는 앱을 만들었다.

여기서 테스트 시 test_config를 통해 DB_URL를 TEST용 DB로 바꿔 줄 것이다.

넷. test 작성

테스트를 작성하기 위해서는 다음을 고려해야한다.

  1. 테스트용 DB를 따로 사용해야한다.
  2. 테스트가 돌때마다 테스트용 DB의 테이블이 초기화되어야한다.
  • 테스트용 DB 적용하기
# in test_main.py
...

import pytest
from main import create_app
from sqlalchemy import create_engine, text
import config

TEST_CONFIG = {
    'TESTING': True,
    'DB_URL': config.TEST_DB_URL
}

@pytest.fixture(scope='session') # 테스트 실행시 한번만 실행
def app():
    app = create_app(TEST_CONFIG)
    return app

@pytest.fixture(scope='session')
def db():
    db = create_engine(TEST_CONFIG['DB_URL'], encoding = 'utf-8', max_overflow = 0)
    return db

@pytest.fixture # 매 테스트 실행 마다 실행
def client(app, db):
    db_file(db, 'test.sql')
    client = app.test_client()
    return client

...

여기서 create_app에 설정해둔 TEST_CONFIG를 넣어주면서 테스트용 DB에서 테스트가 진행될 수 있도록 바꾸었다.

또한 db_file이라는 파일이라는 함수를 작성하여 'test.sql' 정보를 읽어 테스트 함수마다 초기화되도록 fixture function 단위인 client에 넣어주었다.

  • @pytest.fixture
    함수형 테스트에서 사전작업이 필요한 부분들을 묶어 줄 수 있다.
    아무 설정도 하지않으면 매 테스트 마다 실행된다.
  • @pytest.fixture(scope='session')
    테스트 전반에 한번만 수행된다.
    app을 생성하거나 db를 지정하는 동작은 테스트시 한번만 하면되므로 session으로 지정하였다.

test 파일에 다음 항목을 추가해주었다.

다섯. sql 파일 만들기

UserView를 테스트 하기 위해서
users 테이블를 만들고 한명의 유저를 Insert하는 sql를 넣었다.

-- in test.sql
DROP TABLE users;
CREATE TABLE users
(
    `id`             INT             NOT NULL    AUTO_INCREMENT COMMENT 'pk',
    `name`           VARCHAR(100)    NOT NULL    COMMENT '유저이름',
    PRIMARY KEY (id)
);
INSERT INTO users (id, name) VALUES (1, 'user1');
  • 테스트
    중간중간 자신의 일이 잘진행되고 있는지 확인하는것은 시간을 매우 아껴준다.
#mysql 접속
>> source test.sql
>> desc users

다섯. test 작성 및 실행

이제 실제 UserView를 테스트하는 함수를 작성해보자.

# in test_main.py
...
def test_get_user(client):
    response = client.get('/user')
    assert response.status_code == 200
    assert response.json == { 'users': [{'id': 1, 'name':'user1'}] } 

이렇게 하면 test.sql에서 만들어진 user1의 유저를 가져온다.

작성할 것은 다 작성했다. 실행해보자.

pytest -s

테스트가 잘 통과되었다.

DB에 user1이 잘들어왔는지도 확인해보자.

이런 방식으로 테스트DB를 원하는대로 세팅하고 테스트를 진행하면된다.

기타

  • pytest는 test_* 파일을 자동으로 읽어온다. 테스트파일은 반드시 test_로 시작하도록 한다.
  • fuction도 동일하게 def test_로 시작하도록 한다.

전체 파일

main.py

# in main.py
from flask import Flask, jsonify, request
from flask.views import MethodView

from sqlalchemy    import create_engine, text

import config

def create_app(test_config=None):
    app = Flask(__name__)

    app.config.from_pyfile("config.py")

    if test_config:
        app.config.update(test_config)

    db = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0)

    # 뷰
    class UserListView(MethodView):
        def __init__(self, database):
            self.db = database

        def get(self):
            users = self.db.execute("SELECT * FROM users")
            return jsonify({'users': [dict(row) for row in users] }), 200

    # 라우팅
    app.add_url_rule('/user', view_func=UserListView.as_view('user',db))
    return app

if __name__ == '__main__':
    app = create_app()
    app.run(host='0.0.0.0', debug=True, port=8000)

tast_main.py

# in test_main.py
import pytest
from main import create_app
from sqlalchemy import create_engine, text
import config

TEST_CONFIG = {
    'TESTING': True,
    'DB_URL': config.TEST_DB_URL
}

def db_file(db, filename):
    sql_lines = []
    with open(filename, 'r') as file_data:
        # .sql를 주석을 제외하고 라인별로 분류
        sql_lines = [line.strip('\n') for line in file_data if not line.startswith('--') and line.strip('\n')]

        with db.connect() as conn:
            sql_command = ''
            for line in sql_lines:
                sql_command += line
                # ; 나오면 execute
                if sql_command.endswith(';'):
                    try:
                        conn.execute(text(sql_command))
                    except Exception as e:
                        print('Fail DB Reset!!')
                        print(e)
                        return False
                    finally:
                        sql_command = ''
    return True

@pytest.fixture(scope='session') # 테스트 실행시 한번만 실행
def app():
    app = create_app(TEST_CONFIG)
    return app

@pytest.fixture(scope='session')
def db():
    db = create_engine(TEST_CONFIG['DB_URL'], encoding = 'utf-8', max_overflow = 0)
    return db

@pytest.fixture # 매 테스트 실행 마다 실행
def client(app, db):
    db_file(db, 'test.sql')
    client = app.test_client()
    return client

def test_get_user(client):
    response = client.get('/user')
    assert response.status_code == 200
    assert response.json == { 'users': [{'id': 1, 'name':'user1'}] } 

config.py

DB = {
	'host': 'localhost',
	'user': 'root',
	'pass': '',
	'db': 'my_flask'
}

DB_URL = f'mysql://{DB["user"]}:{DB["pass"]}@{DB["host"]}/{DB["db"]}?charset=utf8'

TEST_DB = 'test'

TEST_DB_URL = f'mysql://{DB["user"]}:{DB["pass"]}@{DB["host"]}/{TEST_DB}?charset=utf8'

test.sql

DROP TABLE users;
CREATE TABLE users
(
    `id`             INT             NOT NULL    AUTO_INCREMENT COMMENT 'pk',
    `name`           VARCHAR(100)    NOT NULL    COMMENT '유저이름',
    PRIMARY KEY (id)
);
INSERT INTO users (id, name) VALUES (1, 'user1');
post-custom-banner

0개의 댓글