깔끔한 파이썬 탄탄한 백엔드 -8-

김재민·2020년 5월 3일
1
post-thumbnail

8. Unit Test

기능을 개발하고 나서 제대로 돌아가는지 매번 수동으로 API를 쏠수 없으니 Unit Test를 하여 각 기능들이 제대로 돌아가는지 테스트를 해야합니다.

UI test / End-to-End Test

웹브라우저를 통해서 웹사이트를 실제로 접속하고 UI에 직접 입력하고 클릭하는 등을 통해서 기능을 정상적으로 작동하고 화면이 정상적으로 작동하는지 테스트 해보는 방식

아래는 웹 UI test하는 툴입니다.

  • Selenium
  • Katalon
  • Protractor
  • CasperJS
  • PhantomJS

Integration test(통합 테스트)

각 기능(모듈)을 통합하는 과정에서 모듈간 호환성의 문제를 찾아내기 위해 수행되는 테스트

  • 빅뱅 통합 : 전체 모듈을 모두 통합한 이후 통합 테스트를 수행하는 방식. (서비스 전체를 한번에 유저 시나리오로 테스트 하는 방식)
  • 점진적 통합 : 한번에 모듈을 통합하지 않고 하향식/상향식 기법으로 점진적으로 통합하면서 테스트 하는 방식 (시나리오 하나씩 붙이면서 테스트를 하는 방식)

Unit Test

각각 기능 하나에 대해서 코드 테스트를 합니다.

python3에서 uniittest라는 모듈이 탑재되어 있지만 좀더 쓰기 편한 pytest를 사용합니다.

  • 테스트를 할 파일명은 test_ 를 붙여야 합니다.
# test_minitter.py
import pytest
import bcrypt
import json
import config
import sys
from sqlalchemy import create_engine, text
sys.path.append('../')
from app import create_app

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

@pytest.fixture
def api():
    app = create_app(config.test_config)
    app.config['TESTING'] = True
    api = app.test_client()

    return api

def setup_function():
    hashed_password = bcrypt.hashpw(b"test password", bcrypt.gensalt())
    new_users = [
        {
            "id": 1,
            "name": "test1",
            "email": "test1@test.com",
            "profile": "test1 profile",
            "hashed_password": hashed_password
        },
        {
            "id": 2,
            "name": "test2",
            "email": "test2@test.com",
            "profile": "test2 profile",
            "hashed_password": hashed_password
        }
    ]

    database.execute(text("""
        INSERT INTO users (
            id,
            name,
            email,
            profile,
            hashed_password
        ) VALUES (
            :id,
            :name,
            :email,
            :profile,
            :hashed_password
        )
    """), new_users)

    database.execute(text("""
        INSERT INTO tweets (
            user_id,
            tweet
        ) VALUES (
            2,
            "Hello World!"
        )
    """))

def teardown_function():
    database.execute(text("SET FOREIGN_KEY_CHECKS=0"))
    database.execute(text("TRUNCATE users"))
    database.execute(text("TRUNCATE tweets"))
    database.execute(text("TRUNCATE users_follow_list"))
    database.execute(text("SET FOREIGN_KEY_CHECKS=1"))

def test_ping(api):
    resp = api.get('/ping')
    assert b'pong' in resp.data

def test_login(api):
    resp = api.post(
        '/login',
        data = json.dumps({'email': 'test1@test.com', 'password': 'test password'}),
        content_type = 'application/json'
    )
    assert b"access_token" in resp.data

def test_unauthorized(api):
    resp = api.post(
        '/tweet',
        data = json.dumps({'tweet': 'Hello World!'}),
        content_type = 'application/json'
    )
    assert resp.status_code == 401

    resp = api.post(
        '/follow',
        data = json.dumps({'follow': 2}),
        content_type = 'application/json'
    )
    assert resp.status_code == 401

    resp = api.post(
        '/unfollow',
        data = json.dumps({'unfollow': 2}),
        content_type = 'application/json'
    )
    assert resp.status_code == 401

def test_tweet(api):
    resp = api.post(
            '/login',
            data = json.dumps({'email': 'test1@test.com', 'password': 'test password'}),
            content_type = 'application/json'
            )
    resp_json = json.loads(resp.data.decode('utf-8'))
    access_token = resp_json['access_token']

    resp = api.post(
        '/tweet',
        data = json.dumps({'tweet': "Hello World!"}),
        content_type = 'application/json',
        headers = {'Authorization': access_token}
    )
    assert resp.status_code == 200

    resp = api.get(f'/timeline/1')
    tweets = json.loads(resp.data.decode('utf-8'))

    assert resp.status_code == 200
    assert tweets == {
        "user_id": 1,
        "timeline": [
            {
                "user_id": 1,
                "tweet": "Hello World!"
            }
        ]
    }

def test_follow(api):
    resp = api.post(
        '/login',
        data = json.dumps({'email': 'test1@test.com', 'password': 'test password'}),
        content_type = 'application/json'
    )
    resp_json = json.loads(resp.data.decode('utf-8'))
    access_token = resp_json['access_token']

    resp = api.get(f'/timeline/1')
    tweets = json.loads(resp.data.decode('utf-8'))

    assert resp.status_code ==200
    assert tweets == {
        'user_id': 1,
        'timeline': []
    }

    resp = api.post(
        '/follow',
        data = json.dumps({'id': 1, 'follow': 2}),
        content_type = 'application/json',
        headers = {'Authorization': access_token}
    )

    assert resp.status_code == 200

    resp = api.get(f'/timeline/1')
    tweets = json.loads(resp.data.decode('utf-8'))

    assert resp.status_code == 200
    assert tweets == {
        'user_id': 1,
        'timeline' : [
            {
                'user_id': 2,
                'tweet': 'Hello World!'
            }
        ]
    }

def test_unfollow(api):
    resp = api.post(
        '/login',
        data = json.dumps({'email': 'test1@test.com', 'password': 'test password'}),
        content_type = 'application/json'
    )
    resp_json = json.loads(resp.data.decode('utf-8'))
    access_token = resp_json['access_token']

    resp = api.post(
        '/follow',
        data = json.dumps({'id': 1, 'follow': 2}),
        content_type = 'application/json',
        headers = {'Authorization': access_token}
    )
    assert resp.status_code == 200

    resp = api.get(f'/timeline/1')
    tweets = json.loads(resp.data.decode('utf-8'))

    assert resp.status_code == 200
    assert tweets == {
        "user_id" : 1,
        "timeline": [
            {
                "user_id": 2,
                "tweet": "Hello World!"
            }
        ]
    }

    resp = api.post(
        '/unfollow',
        data = json.dumps({'id': 1, 'unfollow': 2}),
        content_type = 'application/json',
        headers = {'Authorization': access_token}
    )

    assert resp.status_code == 200

    resp = api.get(f'/timeline/1')
    tweets = json.loads(resp.data.decode('utf-8'))

    assert resp.status_code == 200
    assert tweets == {
        "user_id": 1,
        "timeline": []
    }
  • 위와 같이 작성합니다.
  • assert로 값 비교하여 true이면 pass 입니다.
$> pytest -p no:warnings -vv -s
================================================= test session starts ==================================================
platform linux -- Python 3.6.5, pytest-5.4.1, py-1.8.1, pluggy-0.13.1 -- /project/huggingfaceAPI/venv/bin/python
cachedir: .pytest_cache
rootdir: /project/huggingfaceAPI/test
collected 6 items

test_minitter.py::test_ping PASSED
test_minitter.py::test_login PASSED
test_minitter.py::test_unauthorized PASSED
test_minitter.py::test_tweet PASSED
test_minitter.py::test_follow PASSED
test_minitter.py::test_unfollow PASSED

================================================== 6 passed in 11.87s ==================================================
  • 위와 같이 깔끔하게 나오면 됩니다.
  • teardown_function 함수로 test시작 전/후에 실행시킵니다. 내용은 DB 초기화 입니다.
profile
Front end

8개의 댓글

comment-user-thumbnail
2020년 9월 29일

잘보고 있습니다~!! 책으로 하는데 너무 에러가 많아서 에러 고치느라 애먹었는데 글 보며 어느정도 해결했네요! 다음편도 부탁드립니다~!!

2개의 답글