Flask에서 단위 테스트를 실행하는 方法

소개

소프트웨어 개발 과정에서는 테스트가 필수적입니다. 코드가 예상대로 동작하고 결함이 없는지 확인하는데 사용되며, 파이썬에서 pytest는 표준 unit test 모듈을 뛰어넘는 여러 가지 장점을 가진 인기 있는 테스트 프레임워크입니다. pytest는 간결한 문법, 우수한 출력, 강력한 픽tures 및 풍부한 플러그인 생태계를 제공합니다. 이 튜토리얼에서는 Flask 애플리케이션을 설정하고 pytest 픽tures를 통합하며 pytest를 사용하여 유닛 테스트를 작성하는 방법을 안내합니다.

준비 사항

시작하기 전에 다음을 준비해야 합니다:

  • 우분투 서버와 sudo 권한을 가진 루트가 아닌 사용자 및 활성화된 방화벽이 필요합니다. 설정 방법에 대한 가이드를 얻으려면 이 목록에서 배포판을 선택하고 초기 서버 설정 가이드를 따르십시오. 우분투의 지원되는 버전을 사용하십시오.

  • Linux 명령어 行에 대한 熟练도。이 가이드를 Linux 명령어 行 튜토리얼에서 보실 수 있습니다.

  • Python 程式的基本理解以及 Python 中的 pytest 测验框架。您可以参考我们关于 PyTest Python 测验框架 的教程,以了解更多关于 pytest 的信息。diy10>
  • Ubuntu 시스템에 Python 3.7 이상을 설치하였습니다. Python 스크립트를 Ubuntu 上で 실행하는 방법을 배우고자 하시면, Ubuntu 上で Python 스크립트 실행하기에 관한 我们的教程을 참조하시기 바랍니다.

为什么 pytestunittest 的更好选择

pytest는 내장 unittest 프레임ework보다 다양한 장점을 제공합니다.:

  • Pytest는 unittest에서 요구되는 더 자세한 방법으로 대신하여, 간단한 assert 陈述句을 사용하여 테스트를 작성할 수 있습니다.

  • 더욱 상세하고 읽기 쉬운 출력 결과를 제공하여 테스트 실패의 원인을 더 쉽게 파악할 수 있습니다.

  • Pytest fixtures는 unittest의 setUptearDown 메서드보다 더 유연하고 재사용 가능한 테스트 설정을 제공합니다.

  • Pytest는 동일한 테스트 함수를 여러 입력 세트와 함께 실행하는 것을 쉽게 만들어 주며, 이는 unittest에서 그렇게 간단하지 않습니다.

  • Pytest는 코드 커버리지 도구부터 병렬 테스트 실행까지 기능을 확장하는 다양한 플러그인을 제공합니다.

  • Pytest는 자동으로 테스트 파일 및 함수들을 발견하여 테스트 스위트를 관리하는 시간과 노력을 절약합니다.

이러한 이점 때문에 pytest은 현대적인 Python 테스트에서 자주 사용되는 的首е 선택이 되는 것이다. 이제 Flask 应用程序를 세팅하고 pytest을 사용하여 유닛 테스트를 썼을 것이다.

단계 1 – 환경 세팅

Ubuntu 24.04는 기본적으로 Python 3를 搭載한다. 탭톱이나 터미널을 열고 Python 3 설치 현황을 다시 한번 확인하기 위해 다음 명령을 실행하자:

root@ubuntu:~# python3 --version
Python 3.12.3

Python 3이 이미 搭載되어 있다면, 이전 명령은 Python 3의 현재 버전을 반환할 것이다. 搭載되어 있지 않다면, 다음 명령을 실행하여 Python 3를 搭載시킨다:

root@ubuntu:~# sudo apt install python3

次に, pip 패키지 설치기를 시스템에 설치해야 한다.

root@ubuntu:~# sudo apt install python3-pip

一旦 pip이 설치되면, Flask를 설치해봐야 한다.

단계 2 – Flask 应用程序 생성

이제 간단한 Flask 应用程序을 만들어 봐도 좋다. 프로젝트를 위한 새로운 디렉터리를 만들고 그 안으로 이동하자:

root@ubuntu:~# flask_testing_app 디렉터리 생성
root@ubuntu:~# flask_testing_app 디렉터리로 이동

이제 의존성을 관리하기 위해 가상 환경을 생성하고 활성화합시다:

root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# venv/bin/activate 실행하여 가상 환경 활성화

pip를 사용하여 Flask 설치합니다:

root@ubuntu:~# pip install Flask

이제 간단한 Flask 애플리케이션을 생성합시다. app.py라는 새 파일을 만들고 다음 코드를 추가합니다:

app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello, Flask!")

@app.route('/about')
def about():
    return jsonify(message="This is the About page")

@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
    result = x * y
    return jsonify(result=result)

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

이 애플리케이션은 세 가지 라우트를 가지고 있습니다:

  • /: 간단한 “Hello, Flask!” 메시지 반환.
  • /about: 간단한 “This is the About page” 메시지 반환.
  • /multiply/<int:x>/<int:y>: 두 정수를 곱한 결과를 반환.

애플리케이션을 실행하려면 다음 명령을 실행합니다:

root@ubuntu:~# flask run
output
* Serving Flask app "app" (lazy loading) * Environment: production WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. * Debug mode: on * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

위 출력에서 서버가 http://127.0.0.1에서 실행되고 5000번 포트를 감시하고 있음을 알 수 있습니다. 다른 Ubuntu 콘솔을 열고 하나하나 다음 curl 명령을 실행합니다:

  • GET: curl http://127.0.0.1:5000/
  • GET: curl http://127.0.0.1:5000/about
  • GET: curl http://127.0.0.1:5000/multiply/10/20

GET 요청이 하는 일을 이해해봅시다:

  1. curl http://127.0.0.1:5000/:
    이 명령은 Flask 어플리케이션의 루트 경로(‘/’)로 GET 요청을 보냅니다. 서버는 JSON 객체를 반환하며 메시지 “Hello, Flask!”를 포함하여 홈 경로의 기본 기능을 보여줍니다.

  2. curl http://127.0.0.1:5000/about:
    이 명령은 /about 경로로 GET 요청을 보냅니다. 서버는 JSON 객체를 반환하며 메시지 “This is the About page”를 포함하여 경로가 제대로 작동하는 것을 보여줍니다.

  3. curl http://127.0.0.1:5000/multiply/10/20:
    이 명령은 두 개의 매개변수(10과 20)와 함께 /multiply 경로로 GET 요청을 보냅니다. 서버는 이 숫자를 곱한 결과(200)를 포함하는 JSON 객체로 응답합니다. 이는 우리의 곱셈 경로가 URL 매개변수를 제대로 처리하고 계산을 수행할 수 있음을 보여줍니다.

이러한 GET 요청은 Flask 응용 프로그램의 API 엔드 포인트와 상호 작용하는 것을 허용하며, 데이터를 변경하지 않고 서버에 대한 정보를 가져오거나 기능을 실행할 수 있다. 이들은 데이터 가져오기, 엔드 포인트 기능 테스트, Route가 기대되는 응답을 확인하는 것에 유용하다.

이러한 모든 GET 요청을 보여주는 것으로 보자:

root@ubuntu:~# curl http://127.0.0.1:5000/
Output
{"message":"Hello, Flask!"}
root@ubuntu:~# curl http://127.0.0.1:5000/about
Output
{"message":"This is the About page"}
root@ubuntu:~# curl http://127.0.0.1:5000/multiply/10/20
Output
{"result":200}

Step 3 – Installing pytest and Writing Your First Test

이제 Flask 기본 응용 프로그램을 가지고 있으므로 pytest을 설치하고 유닛 테스트를 쓰자.

pytestpip를 사용하여 설치하는 것:

root@ubuntu:~# pip install pytest

유닛 테스트 파일을 저장하기 위한 tests 디렉터리를 만들자:

root@ubuntu:~# 테스트 디렉터리 생성

이제 새 파일을 만들어 test_app.py라고 이름지어 다음 코드를 추가하세요:

test_app.py
# 파이썬의 런타임 환경을 수정하기 위해 sys 모듈을 가져오기
import sys
# 운영 체제와 상호 작용하기 위해 os 모듈을 가져오기
import os

# sys.path에 부모 디렉터리를 추가하기
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# 주 앱 파일에서 Flask 앱 인스턴스를 가져오기
from app import app 
# 테스트 작성 및 실행하기 위해 pytest를 가져오기
import pytest

@pytest.fixture
def client():
    """A test client for the app."""
    with app.test_client() as client:
        yield client

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

이 테스트 파일의 함수를 분석해보겠습니다:

  1. @pytest.fixture def client():
    이것은 우리의 Flask 앱을 위한 테스트 클라이언트를 만드는 pytest 픽스처입니다. app.test_client() 메서드를 사용하여 실제 서버를 실행하지 않고 앱에 요청을 보낼 수 있는 클라이언트를 만듭니다. yield 문은 클라이언트가 테스트에서 사용되고 각 테스트 후에 적절하게 닫히도록 합니다.

  2. def test_home(client):

    이 함수는 우리 应用程序의 主页路由(/)를 테스트합니다. 이 路由에 GET 요청을 发送하고 테스트 客户端을 사용하여, 응답 상태 코드가 200(OK)이며 JSON 응답이 기대되는 消息와 일치하는지 확인합니다.

  3. def test_about(client):

    test_home과 유사하게, 이 함수는 關於路由(/about)를 테스트합니다. 200 상태 코드를 확인하고 JSON 응답 내용을 인증합니다.

  4. def test_multiply(client):

    이 함수는 유효한 인자를 갖는 곱하기 路由(/multiply/3/4)를 테스트합니다. 상태 코드가 200이고 JSON 응답에 곱하기 결과가 정확하게 들어있는지 확인합니다.

  5. def test_multiply_invalid_input(client):
    이 함수는 無効한 입력(multiply/three/four)으로 덧셈 경로를 시험합니다. 이 경로가 문자열 입력을 필요한 정수 매개변수와 일치시키지 못하는 경우 기대되는 행동이 제대로 실행되었는지 확인하기 위해 상태 코드가 404(Not Found)가 되었는지 확인합니다.

  6. def test_non_existent_route(client):
    이 함수는 存在하지 않는 경로를 사용하여 어떻게 어플리케이션이 응답할지 시험합니다. 우리의 Flask 어플리케이션에 정의되지 않은 /non-existent에 GET 요청을 보냅니다. 시험은 응답 상태 코드가 404(Not Found)가 되었다는 것을 인정하는 것으로, 우리의 어플리케이션이 정의되지 않은 경로에 대한 요청을 적절하게 처리하는지 확인합니다.

이러한 시험은 우리의 Flask 어플리케이션의 기본 기능을 涵蓋하여, 각 경로가 유효한 입력에 대해 제대로 응답하고 덧셈 경로가 無効한 입력에 대해 적절하게 처리되는지 확인합니다. pytest을 사용하여 이러한 시험을 쉽게 실행하여 우리의 어플리케이션이 기대되는 것처럼 동작하는지 확인할 수 있습니다.

Step 4 – 실험 실행

실험을 실행하려면 다음 명령을 실행하세요:

root@ubuntu:~# pytest

기본적으로, pytest DISCOVERY PROCESS가 현재 폴더 및 그 하위 폴더에 있는 “test_”로 시작하거나 “_test”로 끝나는 이름의 파일을 재귀적으로 스캔하게 되며, 이러한 파일에 위치하는 모든 실험은 실행되게 됩니다. 다음과 같은 출력을 보게 됩니다:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app collected 5 items tests/test_app.py .... [100%] ======================================================= 5 passed in 0.19s ========================================================

이것은 모든 실험이 성공적으로 통과한 것을 나타냅니다.

Step 5: pytest에서 FiXTURE를 사용하는 방법

FiXTURE는 모듈을 실험에서 사용할 수 있는 데이터 또는 자원을 제공하는 함수입니다. 이를 사용하여 테스트 환경을 설정하고 제거하는 것, 데이터를 로드하는 것이나 다른 세팅 작업을 수행할 수 있습니다. pytest에서는 @pytest.fixture 데코레이터를 사용하여 FiXTURE를 정의합니다.

이제 기존FiXTURE를 향상시키는 방법을 보여드릴 것입니다. 클라이언트FiXTURE를 세팅하고 해제 로직을 사용하여 수정합니다:

test_app.py
@pytest.fixture
def client():
    """Set up a test client for the app with setup and teardown logic."""
    print("\nSetting up the test client")
    with app.test_client() as client:
        yield client  # 여기에 테스트가 이뤄집니다.
    print("Tearing down the test client")

def test_home(client):
    """Test the home route."""
    response = client.get('/')
    assert response.status_code == 200
    assert response.json == {"message": "Hello, Flask!"}

def test_about(client):
    """Test the about route."""
    response = client.get('/about')
    assert response.status_code == 200
    assert response.json == {"message": "This is the About page"}

def test_multiply(client):
    """Test the multiply route with valid input."""
    response = client.get('/multiply/3/4')
    assert response.status_code == 200
    assert response.json == {"result": 12}

def test_multiply_invalid_input(client):
    """Test the multiply route with invalid input."""
    response = client.get('/multiply/three/four')
    assert response.status_code == 404

def test_non_existent_route(client):
    """Test for a non-existent route."""
    response = client.get('/non-existent')
    assert response.status_code == 404

이 셋팅은 printer 문자를 추가하여 integration 과 teardown phases 을 표시하는 것을 보여줍니다. 이러한 문자는 필요하다면 실제 resource management 코드로 대체 할 수 있습니다.

다시 시험을 실행해 봅시다:

root@ubuntu:~ # pytest -vs 

-v flag 은 verbosity를 높이고, -s flag 은 print 문제가 콘솔 输出行에 보여지도록 합니다.

다음과 같은 输出行을 보여야 합니다:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 5 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client ============================================ 5 passed in 0.35s =============================================

Step 6: Adding a Failure Test Case

现存的测试文件中添加一个失败测试用例。修改 test_app.py 文件,在文件末尾添加以下函数,实现结果错误的失败测试用例:

test_app.py
def test_multiply_edge_cases(client):
    """Test the multiply route with edge cases to demonstrate failing tests."""
     # Test with zero 
    response = client.get('/multiply/0/5')
    assert response.status_code == 200
    assert response.json == {"result": 0}

     # Test with large numbers (this might fail if not handled properly) 
    response = client.get('/multiply/1000000/1000000')
    assert response.status_code == 200
    assert response.json == {"result": 1000000000000}

     # Intentional failing test: incorrect result 
    response = client.get('/multiply/2/3')
    assert response.status_code == 200
    assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case"

让我们来分解一下 test_multiply_edge_cases 函数并解释每一部分的作用:

  1. Test with zero:
    This test checks if the multiply function correctly handles multiplication by zero. We expect the result to be 0 when multiplying any number by zero. This is an important edge case to test because some implementations might have issues with zero multiplication.

  2. 대량의 수로의 테스트:
    이 테스트는 곱하기 함수가 오버플로우나 정밀도 문제 없이 대량의 수를 처리할 수 있는지 확인합니다. 100만 값을 두 개를 곱해 10억의 결과를 기대합니다. 이 테스트는 함수의 기능上限을 검사하는 중요한 테스트입니다. 서버의 구현이 대량의 수를 제대로 처리하지 못할 수 있으므로, 대수 라이브러리나 다른 데이터 유형의 필요성을 나타내는可能性が 있습니다.

  3. 의도적으로 실패하는 테스트:
    이 테스트는 고의적으로 실패하도록 설정되었습니다. 2 * 3이 7과 같은지 검사합니다. 이는 틀립니다. 이 테스트는 실패하는 테스트가 출력에 어떻게 보이는지 보여주는 데 사용되며, 테스트 주도 개발과 디버깅과정에서 실패하는 테스트를 식별하고 디버그하는 방법을 이해하는 데 도움이 됩니다.

이러한 에지 케이스와 의도적인 실패를 포함함으로써, 우리는 배수 경로의 기본 기능을 테스트하는 것 뿐만 아니라 극단적인 조건에서의 동작과 오류 보고 능력도 테스트합니다. 이러한 테스트 접근법은 우리 응용 프로그램의 견고성과 신뢰성을 보장하는 데 도움이 됩니다.

다시 테스트를 실행해 보겠습니다:

root@ubuntu:~# pytest -vs

다음과 같은 출력이 보입니다:

Output
platform linux -- Python 3.12.3, pytest-8.3.2, pluggy-1.5.0 rootdir: /home/user/flask_testing_app cachedir: .pytest_cache collected 6 items tests/test_app.py::test_home Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_about Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_invalid_input Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_non_existent_route Setting up the test client PASSED Tearing down the test client tests/test_app.py::test_multiply_edge_cases Setting up the test client FAILED Tearing down the test client ================================================================= FAILURES ================================================================== _________________________________________________________ test_multiply_edge_cases __________________________________________________________ client = <FlaskClient <Flask 'app'>> def test_multiply_edge_cases(client): """Test the multiply route with edge cases to demonstrate failing tests.""" # 0으로의 테스트 response = client.get('/multiply/0/5') assert response.status_code == 200 assert response.json == {"result": 0} # 대용량 숫자로의 테스트 (적절하게 처리되지 않으면 실패할 수 있음) response = client.get('/multiply/1000000/1000000') assert response.status_code == 200 assert response.json == {"result": 1000000000000} # 의도적인 실패 테스트: 결과가 틀림 response = client.get('/multiply/2/3') assert response.status_code == 200 > assert response.json == {"result": 7}, "This test should fail to demonstrate a failing case" E AssertionError: This test should fail to demonstrate a failing case E assert {'result': 6} == {'result': 7} E E Differing items: E {'result': 6} != {'result': 7} E E Full diff: E { E - 'result': 7,... E E ...Full output truncated (4 lines hidden), use '-vv' to show tests/test_app.py:61: AssertionError ========================================================== short test summary info ========================================================== FAILED tests/test_app.py::test_multiply_edge_cases - AssertionError: This test should fail to demonstrate a failing case ======================================================== 1 failed, 5 passed in 0.32s ========================================================

위의 실패 메시지는 tests/test_app.py 파일의 test_multiply_edge_cases 테스트가 실패했음을 나타냅니다. 특히, 이 테스트 함수의 마지막 어서션 때문에 실패했습니다.

이 의도적인 실패는 테스트 실패가 어떻게 보고되는지와 실패 메시지에서 제공되는 정보를 보여주는 데 유용합니다. 실패가 발생한 정확한 줄, 예상값과 실제값, 그리고 두 값의 차이를 보여줍니다.

실제 세계 상황에서는 코드를 수정하여 테스트를 통과 시키거나, 기대 결과가 incorreect이면 테스트를 조정하ます. 然而, 이 경우에는 교육 目的으로 의도적인 실패입니다.

결론

이 튜토리얼에서는 pytest를 사용하여 Flask 应用程序에 대한 유닛 테스트를 세팅하는 方法, pytest fixture를 통합하는 방법, 테스트 실패가 어떻게 보이는지 보여주는 것을 이어 왔습니다. 이러한 단계를 따르면 Flask 应用程序을 신뢰할 수 있고 유지 관리가 可能하게 만들 수 있습니다. 이러한 방법으로 虫이 최소化하고 코드 品質를 향상시킵니다.

더 많은 정보를 얻으시려면 FlaskPytest 공식 文档을 참조하시기 바랍니다.

Source:
https://www.digitalocean.com/community/tutorials/unit-test-in-flask