diff --git a/.dockerignore b/.dockerignore index 8c60a47..6657221 100644 --- a/.dockerignore +++ b/.dockerignore @@ -44,3 +44,4 @@ reports/ *.pb.go *.pb.py + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e22d47..8ea5d8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,237 +1,115 @@ -name: CI +name: CI Tests on: push: - branches: [main, develop] + branches: [ main, master, develop ] pull_request: - branches: [main, develop] + branches: [ main, master, develop ] + workflow_dispatch: jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - cache: 'pip' - - - name: Install dependencies - run: | - pip install --upgrade pip setuptools wheel - pip install -e ".[dev]" - - - name: Run ruff - run: ruff check . - - - name: Run mypy - run: mypy src config || true - test: runs-on: ubuntu-latest strategy: matrix: - test-suite: [smoke, rest, graphql, grpc, database, kafka] - fail-fast: false - - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_DB: api_tests - POSTGRES_USER: tester - POSTGRES_PASSWORD: testpass - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - zookeeper: - image: confluentinc/cp-zookeeper:7.5.0 - env: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - options: >- - --health-cmd "nc -z localhost 2181" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - kafka: - image: confluentinc/cp-kafka:7.5.0 - env: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: localhost:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" - ports: - - 9092:9092 - options: >- - --health-cmd "kafka-broker-api-versions --bootstrap-server localhost:9092" - --health-interval 10s - --health-timeout 10s - --health-retries 10 - + python-version: ["3.11", "3.12"] + steps: - uses: actions/checkout@v4 - - - name: Set up Python + + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: ${{ matrix.python-version }} cache: 'pip' - + - name: Install dependencies run: | - pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip pip install -e ".[dev]" - - - name: Install service check tools + + - name: Run tests run: | - sudo apt-get update - sudo apt-get install -y postgresql-client netcat-openbsd - - - name: Wait for services + pytest -m "rest or graphql or grpc" --alluredir=reports/allure-results + + - name: Install Allure CLI + if: always() run: | - timeout 60 bash -c 'until pg_isready -h localhost -p 5432 -U tester; do sleep 1; done' - timeout 60 bash -c 'until nc -z localhost 2181; do sleep 1; done' - sleep 5 - timeout 60 bash -c 'until nc -z localhost 9092; do sleep 1; done' - - - name: Run tests + wget -q https://github.com/allure-framework/allure2/releases/download/2.24.1/allure-2.24.1.tgz + tar -xzf allure-2.24.1.tgz + sudo mv allure-2.24.1 /opt/allure + sudo ln -s /opt/allure/bin/allure /usr/local/bin/allure + + - name: Generate Allure report + if: always() run: | - pytest -m ${{ matrix.test-suite }} \ - --alluredir=reports/allure-results \ - --junit-xml=reports/junit-${{ matrix.test-suite }}.xml \ - -v - continue-on-error: true - - - name: Upload test results + allure generate reports/allure-results --clean -o reports/allure-report || true + + - name: Upload Allure results if: always() uses: actions/upload-artifact@v4 with: - name: test-results-${{ matrix.test-suite }} - path: | - reports/allure-results - reports/junit-${{ matrix.test-suite }}.xml - retention-days: 7 + name: allure-results-${{ matrix.python-version }} + path: reports/allure-results + retention-days: 30 - test-all: + - name: Upload Allure report + if: always() + uses: actions/upload-artifact@v4 + with: + name: allure-report-${{ matrix.python-version }} + path: reports/allure-report + retention-days: 30 + + publish-pages: + needs: test runs-on: ubuntu-latest - needs: [lint] - - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_DB: api_tests - POSTGRES_USER: tester - POSTGRES_PASSWORD: testpass - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - zookeeper: - image: confluentinc/cp-zookeeper:7.5.0 - env: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - ports: - - 2181:2181 - options: >- - --health-cmd "nc -z localhost 2181" - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - kafka: - image: confluentinc/cp-kafka:7.5.0 - env: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: localhost:2181 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" - ports: - - 9092:9092 - options: >- - --health-cmd "kafka-broker-api-versions --bootstrap-server localhost:9092" - --health-interval 10s - --health-timeout 10s - --health-retries 10 - + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + permissions: + contents: read + pages: write + id-token: write + steps: - uses: actions/checkout@v4 - + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: "3.11" cache: 'pip' - + - name: Install dependencies run: | - pip install --upgrade pip setuptools wheel + python -m pip install --upgrade pip pip install -e ".[dev]" - - - name: Install service check tools - run: | - sudo apt-get update - sudo apt-get install -y postgresql-client netcat-openbsd - - - name: Wait for services - run: | - timeout 60 bash -c 'until pg_isready -h localhost -p 5432 -U tester; do sleep 1; done' - timeout 60 bash -c 'until nc -z localhost 2181; do sleep 1; done' - sleep 5 - timeout 60 bash -c 'until nc -z localhost 9092; do sleep 1; done' - - - name: Run all tests - run: | - pytest \ - --alluredir=reports/allure-results \ - --junit-xml=reports/junit.xml \ - -v - - - name: Generate Allure report - if: always() - uses: simple-elf/allure-report-action@master - with: - allure_results: reports/allure-results - allure_history: reports/allure-history - keep_reports: 20 - continue-on-error: true - - - name: Upload Allure report - if: always() - uses: actions/upload-artifact@v4 + + - name: Download Allure results + uses: actions/download-artifact@v4 with: - name: allure-report + pattern: allure-results-* + merge-multiple: true path: reports/allure-results - retention-days: 30 - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 + + - name: Install Allure CLI + run: | + wget -q https://github.com/allure-framework/allure2/releases/download/2.24.1/allure-2.24.1.tgz + tar -xzf allure-2.24.1.tgz + sudo mv allure-2.24.1 /opt/allure + sudo ln -s /opt/allure/bin/allure /usr/local/bin/allure + + - name: Generate Allure report + run: | + allure generate reports/allure-results --clean -o reports/allure-report + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 with: - name: test-results - path: reports/junit.xml - retention-days: 30 + path: reports/allure-report + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Dockerfile b/Dockerfile index 058805d..41fe868 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,3 +19,4 @@ RUN pip install -e . CMD ["pytest"] + diff --git a/README.md b/README.md index 3fd9221..a8580d7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # API Testing Framework -Python фреймворк для тестирования REST, GraphQL, gRPC API и Kafka. +API testing framework in Python: REST, GraphQL, gRPC, Kafka, PostgreSQL, pytest, Allure. ![Python](https://img.shields.io/badge/Python-3.11+-blue) ![pytest](https://img.shields.io/badge/pytest-8.0+-green) ![License](https://img.shields.io/badge/license-MIT-blue) +![CI](https://github.com/Obrams/api_test_python/actions/workflows/ci.yml/badge.svg) + +**Topics:** `api-testing` `pytest` `grpc` `kafka` `graphql` `sqlalchemy` `allure` ## Содержание @@ -290,6 +293,25 @@ pytest -m "not slow" pytest -m negative ``` +## CI и отчёты + +### GitHub Actions + +CI автоматически запускает тесты REST, GraphQL и gRPC при каждом push и pull request. + +**Статус CI:** [![CI](https://github.com/Obrams/api_test_python/actions/workflows/ci.yml/badge.svg)](https://github.com/Obrams/api_test_python/actions/workflows/ci.yml) + +### Allure отчёты + +- **Артефакты:** Allure результаты и отчёты доступны в [Actions artifacts](https://github.com/Obrams/api_test_python/actions) после каждого запуска CI +- **GitHub Pages:** Статический Allure отчёт публикуется на [GitHub Pages](https://obrams.github.io/api_test_python/) (только для main/master ветки) + +Для скачивания артефактов: +1. Перейти в [Actions](https://github.com/Obrams/api_test_python/actions) +2. Выбрать нужный workflow run +3. Скачать артефакты `allure-results-*` или `allure-report-*` +4. Для просмотра отчёта локально: `allure serve <путь-к-allure-results>` + ## API для тестирования ### REST API diff --git a/config/__init__.py b/config/__init__.py index 0822c04..fe03544 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -8,3 +8,4 @@ + diff --git a/graphql/queries/countries.graphql b/graphql/queries/countries.graphql index 26ba5bd..0a2fc31 100644 --- a/graphql/queries/countries.graphql +++ b/graphql/queries/countries.graphql @@ -111,3 +111,4 @@ query GetCountriesByCurrency($currency: String!) { + diff --git a/graphql/queries/rickmorty.graphql b/graphql/queries/rickmorty.graphql index 7e17e90..f887f98 100644 --- a/graphql/queries/rickmorty.graphql +++ b/graphql/queries/rickmorty.graphql @@ -196,3 +196,4 @@ query SearchCharacters($name: String!) { + diff --git a/proto/hello.proto b/proto/hello.proto index 3731f5e..0a11d1e 100644 --- a/proto/hello.proto +++ b/proto/hello.proto @@ -43,3 +43,4 @@ message HelloSummary { + diff --git a/proto/user.proto b/proto/user.proto index e6519bf..3b1abb2 100644 --- a/proto/user.proto +++ b/proto/user.proto @@ -100,3 +100,4 @@ message SearchUsersRequest { + diff --git a/pyproject.toml b/pyproject.toml index b64f445..7ed94c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "api-test-python" version = "1.0.0" -description = "API Testing Framework: REST, gRPC, GraphQL - 100+ тестов" +description = "API testing framework in Python: REST, GraphQL, gRPC, Kafka, PostgreSQL, pytest, Allure" readme = "README.md" requires-python = ">=3.11" license = {text = "MIT"} diff --git a/src/__init__.py b/src/__init__.py index 46caaae..bfdee8b 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -5,3 +5,4 @@ + diff --git a/src/clients/__init__.py b/src/clients/__init__.py index 91e6b7c..77defd6 100644 --- a/src/clients/__init__.py +++ b/src/clients/__init__.py @@ -19,3 +19,4 @@ + diff --git a/src/database/__init__.py b/src/database/__init__.py index 21298c5..70fe826 100644 --- a/src/database/__init__.py +++ b/src/database/__init__.py @@ -7,3 +7,4 @@ + diff --git a/src/database/base.py b/src/database/base.py index 0070dd2..847e4b9 100644 --- a/src/database/base.py +++ b/src/database/base.py @@ -9,3 +9,4 @@ class Base(DeclarativeBase): + diff --git a/src/database/models.py b/src/database/models.py index 57da443..bdcf5e5 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -93,3 +93,4 @@ def __repr__(self) -> str: + diff --git a/src/models/__init__.py b/src/models/__init__.py index be837b1..37a204a 100644 --- a/src/models/__init__.py +++ b/src/models/__init__.py @@ -33,3 +33,4 @@ + diff --git a/src/models/auth.py b/src/models/auth.py index 05d2b02..48ff4f0 100644 --- a/src/models/auth.py +++ b/src/models/auth.py @@ -64,3 +64,4 @@ class DummyJSONRefreshRequest(BaseModel): + diff --git a/src/models/comment.py b/src/models/comment.py index 1056863..b400ed5 100644 --- a/src/models/comment.py +++ b/src/models/comment.py @@ -68,3 +68,4 @@ class DummyJSONCommentsResponse(BaseModel): + diff --git a/src/models/kafka.py b/src/models/kafka.py index badc74b..cb422df 100644 --- a/src/models/kafka.py +++ b/src/models/kafka.py @@ -172,3 +172,4 @@ def message_count(self) -> int: + diff --git a/src/models/post.py b/src/models/post.py index c950223..f1f104e 100644 --- a/src/models/post.py +++ b/src/models/post.py @@ -70,3 +70,4 @@ class DummyJSONPostsResponse(BaseModel): + diff --git a/src/models/user.py b/src/models/user.py index 108c6ae..47d8335 100644 --- a/src/models/user.py +++ b/src/models/user.py @@ -128,3 +128,4 @@ class ReqresUpdateUserResponse(BaseModel): + diff --git a/src/utils/__init__.py b/src/utils/__init__.py index f5c9e87..554d7df 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -44,3 +44,4 @@ + diff --git a/src/utils/assertions.py b/src/utils/assertions.py index 7b62987..afc8fd0 100644 --- a/src/utils/assertions.py +++ b/src/utils/assertions.py @@ -204,3 +204,4 @@ def assert_error_response( + diff --git a/src/utils/data_generator.py b/src/utils/data_generator.py index 87c152b..ba91b8d 100644 --- a/src/utils/data_generator.py +++ b/src/utils/data_generator.py @@ -166,3 +166,4 @@ class CommentCreateFactory(ModelFactory): + diff --git a/src/utils/db_assertions.py b/src/utils/db_assertions.py index 1c9ac5f..c170fa9 100644 --- a/src/utils/db_assertions.py +++ b/src/utils/db_assertions.py @@ -286,3 +286,4 @@ def assert_post_matches_api( + diff --git a/src/utils/logger.py b/src/utils/logger.py index c0e05f2..c85caa5 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -132,3 +132,4 @@ def log_request( + diff --git a/tests/__init__.py b/tests/__init__.py index 18fcf69..56ad7ef 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,3 +5,4 @@ + diff --git a/tests/conftest.py b/tests/conftest.py index 02aa804..40780b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -274,3 +274,4 @@ def pytest_collection_modifyitems(items): + diff --git a/tests/database/__init__.py b/tests/database/__init__.py index fef4755..aa0d734 100644 --- a/tests/database/__init__.py +++ b/tests/database/__init__.py @@ -3,3 +3,4 @@ + diff --git a/tests/database/conftest.py b/tests/database/conftest.py index 2b28780..afad67c 100644 --- a/tests/database/conftest.py +++ b/tests/database/conftest.py @@ -57,3 +57,4 @@ def db_with_post(db_client: DatabaseClient) -> dict: + diff --git a/tests/database/test_db_verification.py b/tests/database/test_db_verification.py index 847a1fe..9ee07a0 100644 --- a/tests/database/test_db_verification.py +++ b/tests/database/test_db_verification.py @@ -310,3 +310,4 @@ def test_row_exists_fails_when_not_found( + diff --git a/tests/graphql/__init__.py b/tests/graphql/__init__.py index 3dfd3fe..fe96810 100644 --- a/tests/graphql/__init__.py +++ b/tests/graphql/__init__.py @@ -5,3 +5,4 @@ + diff --git a/tests/graphql/conftest.py b/tests/graphql/conftest.py index 5772def..7413dae 100644 --- a/tests/graphql/conftest.py +++ b/tests/graphql/conftest.py @@ -166,3 +166,4 @@ def rickmorty_locations_query(): + diff --git a/tests/graphql/test_countries.py b/tests/graphql/test_countries.py index d23f40a..827d386 100644 --- a/tests/graphql/test_countries.py +++ b/tests/graphql/test_countries.py @@ -231,3 +231,4 @@ def test_invalid_country_code_format(self, graphql_countries_client, country_by_ + diff --git a/tests/graphql/test_graphql_errors.py b/tests/graphql/test_graphql_errors.py index a0d50a1..ebc16c2 100644 --- a/tests/graphql/test_graphql_errors.py +++ b/tests/graphql/test_graphql_errors.py @@ -149,3 +149,4 @@ def test_raw_request_empty_query(self, graphql_countries_client): + diff --git a/tests/graphql/test_rickmorty.py b/tests/graphql/test_rickmorty.py index 0a5e4fc..c33c2a3 100644 --- a/tests/graphql/test_rickmorty.py +++ b/tests/graphql/test_rickmorty.py @@ -268,3 +268,4 @@ def test_invalid_page_number(self, graphql_rickmorty_client, rickmorty_character + diff --git a/tests/grpc/__init__.py b/tests/grpc/__init__.py index ed5bd19..752de2b 100644 --- a/tests/grpc/__init__.py +++ b/tests/grpc/__init__.py @@ -5,3 +5,4 @@ + diff --git a/tests/grpc/conftest.py b/tests/grpc/conftest.py index c51d91d..4e284c2 100644 --- a/tests/grpc/conftest.py +++ b/tests/grpc/conftest.py @@ -27,3 +27,4 @@ def grpc_metadata(): + diff --git a/tests/grpc/test_grpc_basic.py b/tests/grpc/test_grpc_basic.py index 20881dc..6500add 100644 --- a/tests/grpc/test_grpc_basic.py +++ b/tests/grpc/test_grpc_basic.py @@ -314,3 +314,4 @@ def test_unary_call_with_timeout(self): + diff --git a/tests/grpc/test_grpc_status_codes.py b/tests/grpc/test_grpc_status_codes.py index 72cb821..bba1b67 100644 --- a/tests/grpc/test_grpc_status_codes.py +++ b/tests/grpc/test_grpc_status_codes.py @@ -242,3 +242,4 @@ def test_status_resource_exhausted(self): + diff --git a/tests/grpc/test_grpc_streaming.py b/tests/grpc/test_grpc_streaming.py index eb6b789..13d4bc0 100644 --- a/tests/grpc/test_grpc_streaming.py +++ b/tests/grpc/test_grpc_streaming.py @@ -285,3 +285,4 @@ def test_bidirectional_streaming_various_sizes(self, request_count, response_cou + diff --git a/tests/kafka/conftest.py b/tests/kafka/conftest.py index fd9cfb6..c094674 100644 --- a/tests/kafka/conftest.py +++ b/tests/kafka/conftest.py @@ -224,3 +224,4 @@ def data_generator() -> DataGenerator: + diff --git a/tests/kafka/test_kafka_consumer.py b/tests/kafka/test_kafka_consumer.py index 4f540b4..897af55 100644 --- a/tests/kafka/test_kafka_consumer.py +++ b/tests/kafka/test_kafka_consumer.py @@ -193,3 +193,4 @@ def test_consumer_context_manager( + diff --git a/tests/kafka/test_kafka_integration.py b/tests/kafka/test_kafka_integration.py index f23ff52..9ec9c29 100644 --- a/tests/kafka/test_kafka_integration.py +++ b/tests/kafka/test_kafka_integration.py @@ -210,3 +210,4 @@ def test_assert_message_received( + diff --git a/tests/kafka/test_kafka_producer.py b/tests/kafka/test_kafka_producer.py index 12d34a1..60ca05e 100644 --- a/tests/kafka/test_kafka_producer.py +++ b/tests/kafka/test_kafka_producer.py @@ -147,3 +147,4 @@ def test_producer_stats( + diff --git a/tests/rest/__init__.py b/tests/rest/__init__.py index 89d9415..bae8aa7 100644 --- a/tests/rest/__init__.py +++ b/tests/rest/__init__.py @@ -5,3 +5,4 @@ + diff --git a/tests/rest/conftest.py b/tests/rest/conftest.py index f7ca885..5a26d3a 100644 --- a/tests/rest/conftest.py +++ b/tests/rest/conftest.py @@ -42,3 +42,4 @@ def todos_endpoint() -> str: + diff --git a/tests/rest/test_auth.py b/tests/rest/test_auth.py index f103b59..4456709 100644 --- a/tests/rest/test_auth.py +++ b/tests/rest/test_auth.py @@ -182,3 +182,4 @@ def test_register_undefined_user(self, reqres_client, data_generator): + diff --git a/tests/rest/test_posts_crud.py b/tests/rest/test_posts_crud.py index 394b62a..c75f1f1 100644 --- a/tests/rest/test_posts_crud.py +++ b/tests/rest/test_posts_crud.py @@ -220,3 +220,4 @@ def test_get_posts_invalid_user_filter(self, rest_client, posts_endpoint): + diff --git a/tests/rest/test_reqres_users.py b/tests/rest/test_reqres_users.py index 19f807f..2cfa2d0 100644 --- a/tests/rest/test_reqres_users.py +++ b/tests/rest/test_reqres_users.py @@ -213,3 +213,4 @@ def test_delayed_response(self, reqres_client): + diff --git a/tests/rest/test_users_crud.py b/tests/rest/test_users_crud.py index 4373b23..0b11a1c 100644 --- a/tests/rest/test_users_crud.py +++ b/tests/rest/test_users_crud.py @@ -246,3 +246,4 @@ def test_update_nonexistent_user(self, rest_client, users_endpoint): +