Using pytest from now on

This commit is contained in:
Sergey Petrov
2019-06-02 19:39:40 +03:00
parent fbcf36f955
commit be7438a653
9 changed files with 207 additions and 230 deletions
+2 -3
View File
@@ -6,9 +6,8 @@ local Pipeline(py_version) = {
name: "test", name: "test",
image: "python:" + py_version, image: "python:" + py_version,
commands: [ commands: [
"pip install .", "pip install .[tests]",
"pip install -r requirements-dev.txt", "pytest"
"nosetests"
] ]
} }
] ]
+1 -21
View File
@@ -25,12 +25,6 @@ MANIFEST
.installed.cfg .installed.cfg
*.egg *.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs # Installer logs
pip-log.txt pip-log.txt
pip-delete-this-directory.txt pip-delete-this-directory.txt
@@ -45,28 +39,14 @@ nosetests.xml
coverage.xml coverage.xml
*,cover *,cover
.hypothesis/ .hypothesis/
.pytest_cache/
# Translations # Translations
*.mo *.mo
*.pot *.pot
# Django stuff:
*.log
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/_build/
# PyBuilder
target/
#Ipython Notebook
.ipynb_checkpoints
# IntelliJ IDEA
.idea/
# Virtualenv # Virtualenv
venv/ venv/
# Tox
.tox/
+4 -5
View File
@@ -54,19 +54,18 @@ cd PyAPNs2
# Create a virtualenv and install dependencies. # Create a virtualenv and install dependencies.
virtualenv venv virtualenv venv
. venv/bin/activate . venv/bin/activate
pip install -e . pip install -e .[tests]
``` ```
To run the tests: To run the tests:
```shell ```shell
pip install -r requirements-dev.txt pytest
python -m unittest discover test
``` ```
You can use `tox` for running tests with all supported Python versions: You can use `tox` for running tests with all supported Python versions:
```shell ```shell
pyenv install 2.7.15; pyenv install 3.4.9; pyenv install 3.5.6; pyenv install 3.6.7; pyenv install 3.7.1 pyenv install 3.5.6; pyenv install 3.6.7; pyenv install 3.7.1
pyenv local 3.7.1 3.6.7 3.5.6 3.4.9 2.7.15 pyenv local 3.7.1 3.6.7 3.5.6
pip install tox pip install tox
tox tox
``` ```
-3
View File
@@ -1,3 +0,0 @@
freezegun
nose
mock; python_version < '3.3'
+6
View File
@@ -11,6 +11,12 @@ setup(
'PyJWT>=1.4.0', 'PyJWT>=1.4.0',
'cryptography>=1.7.2', 'cryptography>=1.7.2',
], ],
extras_require={
"tests": [
'freezegun',
'pytest',
],
},
url='https://github.com/Pr0Ger/PyAPNs2', url='https://github.com/Pr0Ger/PyAPNs2',
license='MIT', license='MIT',
author='Sergey Petrov', author='Sergey Petrov',
+97 -82
View File
@@ -1,110 +1,125 @@
# pylint: disable=protected-access
from unittest import TestCase
import contextlib import contextlib
import logging from unittest.mock import MagicMock, Mock, patch
try: import pytest
# Python 3
from unittest.mock import MagicMock, Mock, patch
except ImportError:
# Python 2
from mock import MagicMock, Mock, patch
from apns2.client import APNsClient, CONCURRENT_STREAMS_SAFETY_MAXIMUM, Notification from apns2.client import APNsClient, CONCURRENT_STREAMS_SAFETY_MAXIMUM, Notification
from apns2.errors import ConnectionFailed from apns2.errors import ConnectionFailed
from apns2.payload import Payload from apns2.payload import Payload
TOPIC = 'com.example.App'
class ClientTestCase(TestCase):
@classmethod
def setUpClass(cls):
# Ignore all log messages so that test output is not cluttered.
logging.basicConfig(level=logging.CRITICAL)
cls.tokens = ['%064x' % i for i in range(10000)]
cls.payload = Payload(alert='Test alert')
cls.notifications = [Notification(token=token, payload=cls.payload) for token in cls.tokens]
cls.topic = 'com.example.App'
def setUp(self): @pytest.fixture(scope='session')
self.open_streams = 0 def tokens():
self.max_open_streams = 0 return ['%064x' % i for i in range(1000)]
self.mock_results = None
self.next_stream_id = 0
with patch('apns2.credentials.HTTP20Connection') as mock_connection_constructor, patch('apns2.credentials.init_context'):
self.mock_connection = MagicMock() @pytest.fixture(scope='session')
self.mock_connection.get_response.side_effect = self.mock_get_response def notifications(tokens):
self.mock_connection.request.side_effect = self.mock_request payload = Payload(alert='Test alert')
self.mock_connection._conn.__enter__.return_value = self.mock_connection._conn return [Notification(token=token, payload=payload) for token in tokens]
self.mock_connection._conn.remote_settings.max_concurrent_streams = 500
mock_connection_constructor.return_value = self.mock_connection
self.client = APNsClient(credentials=None) @patch('apns2.credentials.init_context')
@pytest.fixture
def client(mock_connection):
with patch('apns2.credentials.HTTP20Connection') as mock_connection_constructor:
mock_connection_constructor.return_value = mock_connection
return APNsClient(credentials=None)
@pytest.fixture
def mock_connection():
mock_connection = MagicMock()
mock_connection.__max_open_streams = 0
mock_connection.__open_streams = 0
mock_connection.__mock_results = None
mock_connection.__next_stream_id = 0
@contextlib.contextmanager @contextlib.contextmanager
def mock_get_response(self, stream_id): def mock_get_response(stream_id):
self.open_streams -= 1 mock_connection.__open_streams -= 1
if self.mock_results: if mock_connection.__mock_results:
reason = self.mock_results[stream_id] reason = mock_connection.__mock_results[stream_id]
response = Mock(status=200 if reason == 'Success' else 400) response = Mock(status=200 if reason == 'Success' else 400)
response.read.return_value = ('{"reason": "%s"}' % reason).encode('utf-8') response.read.return_value = ('{"reason": "%s"}' % reason).encode('utf-8')
yield response yield response
else: else:
yield Mock(status=200) yield Mock(status=200)
def mock_request(self, *dummy_args): def mock_request(*_args):
self.open_streams += 1 mock_connection.__open_streams += 1
if self.open_streams > self.max_open_streams: mock_connection.__max_open_streams = max(mock_connection.__open_streams, mock_connection.__max_open_streams)
self.max_open_streams = self.open_streams
stream_id = self.next_stream_id stream_id = mock_connection.__next_stream_id
self.next_stream_id += 1 mock_connection.__next_stream_id += 1
return stream_id return stream_id
def test_send_notification_batch_returns_results_in_order(self): mock_connection.get_response.side_effect = mock_get_response
results = self.client.send_notification_batch(self.notifications, self.topic) mock_connection.request.side_effect = mock_request
expected_results = {token: 'Success' for token in self.tokens} mock_connection._conn.__enter__.return_value = mock_connection._conn
self.assertEqual(results, expected_results) mock_connection._conn.remote_settings.max_concurrent_streams = 500
def test_send_notification_batch_respects_max_concurrent_streams_from_server(self): return mock_connection
self.client.send_notification_batch(self.notifications, self.topic)
self.assertEqual(self.max_open_streams, 500)
def test_send_notification_batch_overrides_server_max_concurrent_streams_if_too_large(self):
self.mock_connection._conn.remote_settings.max_concurrent_streams = 5000
self.client.send_notification_batch(self.notifications, self.topic)
self.assertEqual(self.max_open_streams, CONCURRENT_STREAMS_SAFETY_MAXIMUM)
def test_send_notification_batch_overrides_server_max_concurrent_streams_if_too_small(self): def test_connect_establishes_connection(client, mock_connection):
self.mock_connection._conn.remote_settings.max_concurrent_streams = 0 client.connect()
self.client.send_notification_batch(self.notifications, self.topic) mock_connection.connect.assert_called_once_with()
self.assertEqual(self.max_open_streams, 1)
def test_send_notification_batch_reports_different_results(self):
self.mock_results = ( def test_connect_retries_failed_connection(client, mock_connection):
mock_connection.connect.side_effect = [RuntimeError, RuntimeError, None]
client.connect()
assert mock_connection.connect.call_count == 3
def test_connect_stops_on_reaching_max_retries(client, mock_connection):
mock_connection.connect.side_effect = [RuntimeError] * 4
with pytest.raises(ConnectionFailed):
client.connect()
assert mock_connection.connect.call_count == 3
def test_send_empty_batch_does_nothing(client, mock_connection):
client.send_notification_batch([], TOPIC)
assert mock_connection.request.call_count == 0
def test_send_notification_batch_returns_results_in_order(client, mock_connection, tokens, notifications):
results = client.send_notification_batch(notifications, TOPIC)
expected_results = {token: 'Success' for token in tokens}
assert results == expected_results
def test_send_notification_batch_respects_max_concurrent_streams_from_server(client, mock_connection, tokens,
notifications):
client.send_notification_batch(notifications, TOPIC)
assert mock_connection.__max_open_streams == 500
def test_send_notification_batch_overrides_server_max_concurrent_streams_if_too_large(client, mock_connection, tokens,
notifications):
mock_connection._conn.remote_settings.max_concurrent_streams = 5000
client.send_notification_batch(notifications, TOPIC)
assert mock_connection.__max_open_streams == CONCURRENT_STREAMS_SAFETY_MAXIMUM
def test_send_notification_batch_overrides_server_max_concurrent_streams_if_too_small(client, mock_connection, tokens,
notifications):
mock_connection._conn.remote_settings.max_concurrent_streams = 0
client.send_notification_batch(notifications, TOPIC)
assert mock_connection.__max_open_streams == 1
def test_send_notification_batch_reports_different_results(client, mock_connection, tokens,
notifications):
mock_connection.__mock_results = (
['BadDeviceToken'] * 1000 + ['Success'] * 1000 + ['DeviceTokenNotForTopic'] * 2000 + ['BadDeviceToken'] * 1000 + ['Success'] * 1000 + ['DeviceTokenNotForTopic'] * 2000 +
['Success'] * 1000 + ['BadDeviceToken'] * 500 + ['PayloadTooLarge'] * 4500 ['Success'] * 1000 + ['BadDeviceToken'] * 500 + ['PayloadTooLarge'] * 4500
) )
results = self.client.send_notification_batch(self.notifications, self.topic) results = client.send_notification_batch(notifications, TOPIC)
expected_results = dict(zip(self.tokens, self.mock_results)) expected_results = dict(zip(tokens, mock_connection.__mock_results))
self.assertEqual(results, expected_results) assert results == expected_results
def test_send_empty_batch_does_nothing(self):
self.client.send_notification_batch([], self.topic)
self.assertEqual(self.mock_connection.request.call_count, 0)
def test_connect_establishes_connection(self):
self.client.connect()
self.mock_connection.connect.assert_called_once_with()
def test_connect_retries_failed_connection(self):
self.mock_connection.connect.side_effect = [RuntimeError, RuntimeError, None]
self.client.connect()
self.assertEqual(self.mock_connection.connect.call_count, 3)
def test_connect_stops_on_reaching_max_retries(self):
self.mock_connection.connect.side_effect = [RuntimeError] * 4
with self.assertRaises(ConnectionFailed):
self.client.connect()
self.assertEqual(self.mock_connection.connect.call_count, 3)
+10 -33
View File
@@ -4,50 +4,27 @@
# - timing out of the token # - timing out of the token
# - creating multiple tokens for different topics # - creating multiple tokens for different topics
from unittest import TestCase, main import pytest
from freezegun import freeze_time from freezegun import freeze_time
import time
from apns2.credentials import TokenCredentials from apns2.credentials import TokenCredentials
TOPIC = 'com.example.first_app'
class TokenCredentialsTestCase(TestCase):
@classmethod
def setUpClass(cls):
cls.key_path = 'test/eckey.pem'
cls.team_id = '3Z24IP123A'
cls.key_id = '1QBCDJ9RST'
cls.topics = ('com.example.first_app', 'com.example.second_app',)
cls.token_lifetime = 0.5
def setUp(self): @pytest.fixture
# Create an 'ephemeral' token so we can test token timeouts. We def token_credentials():
# want a timeout long enough to last the test, but we don't want to return TokenCredentials('test/eckey.pem', '1QBCDJ9RST', '3Z24IP123A')
# slow down the tests too much either.
self.normal_creds = TokenCredentials(self.key_path, self.key_id,
self.team_id)
self.lasting_header = self.normal_creds.get_authorization_header(
self.topics[0])
with freeze_time('2012-01-14'):
self.expiring_creds = \
TokenCredentials(self.key_path, self.key_id,
self.team_id,
token_lifetime=self.token_lifetime)
self.expiring_header = self.expiring_creds.get_authorization_header(
self.topics[0])
def test_token_expiration(self): def test_token_expiration(token_credentials):
# As long as the token lifetime hasn't elapsed, this should work. To # As long as the token lifetime hasn't elapsed, this should work. To
# be really careful, we should check how much time has elapsed to # be really careful, we should check how much time has elapsed to
# know if it fail. But, either way, we'd have to come up with a good # know if it fail. But, either way, we'd have to come up with a good
# lifetime for future tests... # lifetime for future tests...
time.sleep(self.token_lifetime)
h3 = self.expiring_creds.get_authorization_header(self.topics[0])
self.assertNotEqual(self.expiring_header, h3)
with freeze_time('2012-01-14'):
expiring_header = token_credentials.get_authorization_header(TOPIC)
if __name__ == '__main__': new_header = token_credentials.get_authorization_header(TOPIC)
main() assert expiring_header != new_header
+36 -30
View File
@@ -1,25 +1,43 @@
# pylint: disable=protected-access import pytest
from unittest import TestCase
from apns2.payload import Payload, PayloadAlert from apns2.payload import Payload, PayloadAlert
class PayloadTestCase(TestCase): @pytest.fixture
def payload_alert():
return PayloadAlert(
title='title',
title_localized_key='loc_k',
title_localized_args='loc_a',
body='body',
body_localized_key='body_loc_k',
body_localized_args='body_loc_a',
action_localized_key='ac_loc_k',
action='send',
launch_image='img'
)
def setUp(self):
self.payload_alert = payload_alert = PayloadAlert(
title='title', title_localized_key='loc_k', title_localized_args='loc_a',
body='body', body_localized_key='body_loc_k', body_localized_args='body_loc_a',
action_localized_key='ac_loc_k', action='send',
launch_image='img')
def test_payload(self): def test_payload_alert(payload_alert):
assert payload_alert.dict() == {
'title': 'title',
'title-loc-key': 'loc_k',
'title-loc-args': 'loc_a',
'body': 'body',
'loc-key': 'body_loc_k',
'loc-args': 'body_loc_a',
'action-loc-key': 'ac_loc_k',
'action': 'send',
'launch-image': 'img'
}
def test_payload():
payload = Payload( payload = Payload(
alert='my_alert', badge=2, sound='chime', alert='my_alert', badge=2, sound='chime',
content_available=1, mutable_content=3, content_available=1, mutable_content=3,
category='my_category', url_args='args', custom={'extra': 'something'}, thread_id=42) category='my_category', url_args='args', custom={'extra': 'something'}, thread_id=42)
self.assertEqual(payload.dict(), { assert payload.dict() == {
'aps': { 'aps': {
'alert': 'my_alert', 'alert': 'my_alert',
'badge': 2, 'badge': 2,
@@ -31,14 +49,15 @@ class PayloadTestCase(TestCase):
'url-args': 'args' 'url-args': 'args'
}, },
'extra': 'something' 'extra': 'something'
}) }
def test_payload_with_payload_alert(self):
def test_payload_with_payload_alert(payload_alert):
payload = Payload( payload = Payload(
alert=self.payload_alert, badge=2, sound='chime', alert=payload_alert, badge=2, sound='chime',
content_available=1, mutable_content=1, content_available=1, mutable_content=1,
category='my_category', url_args='args', custom={'extra': 'something'}, thread_id=42) category='my_category', url_args='args', custom={'extra': 'something'}, thread_id=42)
self.assertEqual(payload.dict(), { assert payload.dict() == {
'aps': { 'aps': {
'alert': { 'alert': {
'title': 'title', 'title': 'title',
@@ -60,17 +79,4 @@ class PayloadTestCase(TestCase):
'url-args': 'args', 'url-args': 'args',
}, },
'extra': 'something' 'extra': 'something'
}) }
def test_payload_alert(self):
self.assertEqual(self.payload_alert.dict(), {
'title': 'title',
'title-loc-key': 'loc_k',
'title-loc-args': 'loc_a',
'body': 'body',
'loc-key': 'body_loc_k',
'loc-args': 'body_loc_a',
'action-loc-key': 'ac_loc_k',
'action': 'send',
'launch-image': 'img'
})
+3 -5
View File
@@ -1,11 +1,9 @@
[tox] [tox]
envlist = py27, py35, py36, py37 envlist = py35, py36, py37
[testenv] [testenv]
commands = nosetests [] commands = pytest {posargs}
deps = extras = tests
nose
-rrequirements-dev.txt
usedevelop=True usedevelop=True