Skip to content

Commit

Permalink
Feat: Adding tests and code coverage (#14)
Browse files Browse the repository at this point in the history
* feat: Adding tests and code coverage

* Adding some tests for producer

* Adding more producer tests and pytest action
  • Loading branch information
Shwetabhk authored Nov 18, 2023
1 parent f983a9f commit f4cb36b
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 86 deletions.
10 changes: 10 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# .coveragerc
[run]
omit =
tests/*
*/__init__.py
setup.py
*/main.py

[report]
show_missing=true
47 changes: 47 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Run Pytest and Coverage

on:
push:
branches:
- main
pull_request:
types: [assigned, opened, synchronize, reopened]

jobs:
pytest:
runs-on: ubuntu-latest

steps:
# Checkout the code from the repository
- name: Checkout code
uses: actions/checkout@v2

# Setup Python environment
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8

# Install Python dependencies
- name: Install dependencies
run: pip install -r requirements.txt

# Run Pytest with coverage
- name: Run Tests
run: |
python -m pytest tests/ --cov=. --junit-xml=test-reports/report.xml --cov-report=term-missing --cov-fail-under=70 | tee pytest-coverage.txt
echo "STATUS=$(grep 'Required test' pytest-coverage.txt | awk '{ print $1 }')" >> $GITHUB_ENV
echo "FAILED=$(awk -F'=' '/failures=/ {gsub(/"/, "", $2); print $2}' test-reports/report.xml)" >> $GITHUB_ENV
# Post Pytest coverage as a comment on PR
- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
create-new-comment: true
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./test-reports/report.xml

# Evaluate test and coverage results
- name: Evaluate Coverage
if: env.STATUS == 'FAIL' || env.FAILED > 0
run: exit 1
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ dev.sql
build/
dist/
*.egg-info/

.coverage
5 changes: 3 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ services:
PGDATABASE: dummy
PGUSER: postgres
PGPASSWORD: postgres
PGREPLICATIONSLOT: pgevents_slot
PGOUTPUTPLUGIN: pgoutput
PGREPLICATIONSLOT: pgevents
PGOUTPUTPLUGIN: wal2json
PGTABLES: public.users
PGPUBLICATION: events
RABBITMQ_URL: amqp://admin:password@rabbitmq:5672/?heartbeat=0
Expand All @@ -41,6 +41,7 @@ services:
command: ["bash", "-c", "producer"]
volumes:
- ./producer:/pgevents/producer
- ./test:/pgevents/test
depends_on:
- database
- rabbitmq
Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pytest.ini
[pytest]
addopts = --cov=. --cov-config=.coveragerc
testpaths = tests/
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ py==1.11.0
pylint==2.5.3
pyparsing==3.0.7
pytest==7.0.1
pytest-cov==4.1.0
toml==0.10.2
tomli==2.0.1
wrapt==1.12.1
199 changes: 178 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,192 @@
import os
from unittest import mock
import psycopg2
import pytest
from common.event import base_event

from common.qconnector import RabbitMQConnector
from common import log
from producer.event_producer import EventProducer

logger = log.get_logger(__name__)


@pytest.fixture(scope='function')
def db_conn():
db_connection = psycopg2.connect(
user=os.environ['PGUSER'],
password=os.environ['PGPASSWORD'],
host=os.environ['PGHOST'],
port=os.environ['PGPORT'],
dbname=os.environ['PGDATABASE']
)
@pytest.fixture
def producer_init_params():
return {
'qconnector_cls': RabbitMQConnector,
'event_cls': base_event.BaseEvent,
'pg_host': 'localhost',
'pg_port': 5432,
'pg_database': 'test',
'pg_user': 'test',
'pg_password': 'test',
'pg_replication_slot': 'test',
'pg_output_plugin': 'pgoutput',
'pg_tables': 'public.users',
'pg_publication_name': 'test',
'rabbitmq_url': 'amqp://admin:password@rabbitmq:5672/?heartbeat=0',
'rabbitmq_exchange': 'test'
}

yield db_connection
db_connection.close()

@pytest.fixture
def mock_producer(producer_init_params):
return EventProducer(**producer_init_params)

@pytest.fixture(scope='session')
def rmq_conn():
rmq_connector = RabbitMQConnector(
rabbitmq_url=os.environ['RABBITMQ_URL'],
rabbitmq_exchange=os.environ['RABBITMQ_EXCHANGE'],
queue_name='PRODUCER_TEST_QUEUE',
binding_keys='#'
)

rmq_connector.connect()
return rmq_connector
@pytest.fixture
def mock_pika_connect():
with mock.patch('pika.BlockingConnection') as mock_pika:
yield mock_pika


@pytest.fixture
def mock_pg_conn():
with mock.patch('psycopg2.connect') as mock_pg:
yield mock_pg


@pytest.fixture
def mock_basic_publish():
with mock.patch('pika.adapters.blocking_connection.BlockingChannel.basic_publish') as mock_publish:
yield mock_publish


@pytest.fixture
def mock_schema():
return {
'relation_id': 16385,
'table_name': 'public.users',
'columns': [
{'name': 'id'},
{'name': 'full_name'},
{'name': 'company'},
{'name': 'created_at'},
{'name': 'updated_at'}
]
}

@pytest.fixture
def wal2json_payload():
return {"action":"D","lsn":"0/1671850","schema":"public","table":"users","identity":[{"name":"id","type":"integer","value":2},{"name":"full_name","type":"text","value":"Geezer Butler"},{"name":"company","type":"jsonb","value":"{\"name\": \"Fyle\"}"},{"name":"created_at","type":"timestamp with time zone","value":"2023-11-17 13:35:09.471909+00"},{"name":"updated_at","type":"timestamp with time zone","value":"2023-11-17 13:35:09.471909+00"}]}

# Class to hold payload data
class OutputData:
payload = None
data_start = 124122


# Fixture for relation payload
@pytest.fixture
def relation_payload():
data = OutputData()
data.payload = b'R\x00\x00@\x01public\x00users\x00f\x00\x05\x01id\x00\x00\x00\x00\x17\xff\xff\xff\xff\x01full_name\x00\x00\x00\x00\x19\xff\xff\xff\xff\x01company\x00\x00\x00\x0e\xda\xff\xff\xff\xff\x01created_at\x00\x00\x00\x04\xa0\xff\xff\xff\xff\x01updated_at\x00\x00\x00\x04\xa0\xff\xff\xff\xff'
return data


# Fixture for expected relation response
@pytest.fixture
def relation_response():
return {
'relation_id': 16385,
'table_name': 'public.users',
'columns': [
{'name': 'id'},
{'name': 'full_name'},
{'name': 'company'},
{'name': 'created_at'},
{'name': 'updated_at'}
]
}


# Fixture for insert payload
@pytest.fixture
def insert_payload():
data = OutputData()
data.payload = b'I\x00\x00@\x01N\x00\x05t\x00\x00\x00\x011t\x00\x00\x00\x04Miket\x00\x00\x00\x10{"name": "Fyle"}t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00'
return data


# Fixture for expected insert response
@pytest.fixture
def insert_response():
return {
'table_name': 'public.users',
'new': {
'id': '1',
'full_name': 'Mike',
'company': '{"name": "Fyle"}',
'created_at': '2023-11-17 13:44:14.700844+00',
'updated_at': '2023-11-17 13:44:14.700844+00'
},
'id': '1',
'old': {},
'diff': {
'id': '1',
'full_name': 'Mike',
'company': '{"name": "Fyle"}',
'created_at': '2023-11-17 13:44:14.700844+00',
'updated_at': '2023-11-17 13:44:14.700844+00'
},
'action': 'I'
}

# Fixture for update payload
@pytest.fixture
def update_payload():
data = OutputData()
data.payload = b'U\x00\x00@\x01O\x00\x05t\x00\x00\x00\x011t\x00\x00\x00\x04Miket\x00\x00\x00\x10{"name": "Fyle"}t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00N\x00\x05t\x00\x00\x00\x011t\x00\x00\x00\x05Mylest\x00\x00\x00\x10{"name": "Fyle"}t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00t\x00\x00\x00\x1d2023-11-17 13:44:14.700844+00'
return data

# Fixture for expected update response
@pytest.fixture
def update_response():
return {
'table_name': 'public.users',
'id': '1',
'old': {
'id': '1',
'full_name': 'Mike',
'company': '{"name": "Fyle"}',
'created_at': '2023-11-17 13:44:14.700844+00',
'updated_at': '2023-11-17 13:44:14.700844+00'
},
'new': {
'id': '1',
'full_name': 'Myles',
'company': '{"name": "Fyle"}',
'created_at': '2023-11-17 13:44:14.700844+00',
'updated_at': '2023-11-17 13:44:14.700844+00'
},
'diff': {
'full_name': 'Myles'
},
'action': 'U'
}

# Fixture for delete payload
@pytest.fixture
def delete_payload():
data = OutputData()
data.payload = b'D\x00\x00@\x01O\x00\x05t\x00\x00\x00\x012t\x00\x00\x00\rGeezer Butlert\x00\x00\x00\x10{"name": "Fyle"}t\x00\x00\x00\x1d2023-11-17 13:35:09.471909+00t\x00\x00\x00\x1d2023-11-17 13:35:09.471909+00'
return data

# Fixture for expected delete response
@pytest.fixture
def delete_response():
return {
'table_name': 'public.users',
'action': 'D',
'old': {
'id': '2',
'full_name': 'Geezer Butler',
'company': '{"name": "Fyle"}',
'created_at': '2023-11-17 13:35:09.471909+00',
'updated_at': '2023-11-17 13:35:09.471909+00'
},
'id': '2',
'new': {},
'diff': {}
}
Loading

1 comment on commit f4cb36b

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
common
   compression.py5260%5, 9
   exceptions.py220%1–2
   log.py90100% 
   utils.py80100% 
common/event
   base_event.py1134858%20–25, 35–51, 56–58, 61, 75, 82, 94–100, 114–115, 127–129, 132–135, 142, 145, 148–153
common/qconnector
   q_connector.py28968%14, 18, 22, 26, 30, 33–34, 38–39
   rabbitmq_connector.py452251%23–24, 27–29, 37–49, 52–65, 80–94
consumer
   event_consumer.py33330%1–51
pgoutput_parser
   base.py610100% 
   delete.py140100% 
   insert.py150100% 
   relation.py270100% 
   update.py190100% 
producer
   event_producer.py1262283%67–75, 117–121, 197–205, 211, 214–216, 222–223
TOTAL50513873% 

Tests Skipped Failures Errors Time
10 0 💤 0 ❌ 0 🔥 0.159s ⏱️

Please sign in to comment.