-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
a bit of progress towards a functional runner
- Loading branch information
1 parent
f3b9611
commit 4862925
Showing
13 changed files
with
1,093 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
[draft] Changes in version 1.5.0 | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
* Introducing the concept of BehaviorDefinition: a clean and | ||
decoupled way to reutilize setup/teardown behaviors. So instead of | ||
the classic massive setup/teardown methods and/or chaotic | ||
``unittest.TestCase`` subclass inheritance every test can be | ||
decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) | ||
* Avoid using the word "test" in your "Behavior Definitions" so that | ||
nose will not mistake your BehaviorDefinition with an actual test | ||
case class and thus execute .setup() and .teardown() in an | ||
undesired manner. |
123 changes: 123 additions & 0 deletions
123
examples/functional-tests/behavior_definition_with_live_network_servers.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import json | ||
import requests | ||
from gevent.pool import Pool | ||
from flask import Flask, Response | ||
from sqlalchemy import create_engine | ||
from sqlalchemy import MetaData | ||
|
||
from myapp.db import sqlalchemy_metadata | ||
from sure.scenario import BehaviorDefinition, apply_behavior | ||
|
||
|
||
# Changes in version 1.5.0 [draft] | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# * Introducing the concept of BehaviorDefinition: a clean and | ||
# decoupled way to reutilize setup/teardown behaviors. So instead of | ||
# the classic massive setup/teardown methods and/or chaotic | ||
# ``unittest.TestCase`` subclass inheritance every test can be | ||
# decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) | ||
# | ||
# * Avoid using the word "test" in your "Behavior Definitions" so that | ||
# nose will not mistake your BehaviorDefinition with an actual test | ||
# case class and thus execute .setup() and .teardown() in an | ||
# undesired manner. | ||
|
||
|
||
|
||
def get_json_request(self): | ||
"""parses the request body as JSON without running any sort of validation""" | ||
return json.loads(request.data) | ||
|
||
def json_response(response_body, status=200, headers=None): | ||
"""utility that automatically serializes the provided payload in JSON | ||
and generates :py:`flask.Response` with the ``application/json`` | ||
content-type. | ||
:param response_body: a python dictionary or any JSON-serializable python object. | ||
""" | ||
headers = headers or {} | ||
serialized = json.dumps(response_body, indent=2) | ||
headers[b'Content-Type'] = 'application/json' | ||
return Response(serialized, status=code, headers=headers) | ||
|
||
|
||
class GreenConcurrencyBehaviorDefinition(BehaviorDefinition): | ||
# NOTE: | ||
# ---- | ||
# | ||
# * Sure uses ``context_namespace`` internally to namespace the | ||
# self-assigned attributes into the context in order to prevent | ||
# attribute name collision. | ||
|
||
context_namespace = 'green' | ||
|
||
def setup(self, pool_size=1): | ||
self.pool = Pool(pool_size) | ||
|
||
|
||
class UseFakeHTTPAPIServer(GreenConcurrencyBehaviorDefinition): | ||
context_namespace = 'fake_api' | ||
|
||
def setup(self, http_port): | ||
# NOTES: | ||
# ~~~~~~ | ||
# | ||
# * GreenConcurrencyBehaviorDefinition.setup() is automatically called by | ||
# * sure in the correct order | ||
# | ||
# * Sure automatically takes care of performing top-down calls | ||
# to every parent of your behavior. | ||
# | ||
# * In simple words, this UseFakeHTTPAPIServer behavior will automatically call GreenConcurrencyBehaviorDefinition | ||
|
||
# 1. Create a simple Flask server | ||
self.server = Flask('fake.http.api.server') | ||
# 2. Setup fake routes | ||
self.server.add_url_rule('/auth', view_func=self.fake_auth_endpoint) | ||
self.server.add_url_rule('/item/<uid>', view_func=self.fake_get_item_endpoint) | ||
# 3. Run the server | ||
self.pool.spawn(self..server.run, port=http_port) | ||
|
||
def teardown(self): | ||
self.server.stop() | ||
|
||
def fake_auth_endpoint(self): | ||
data = get_json_request() | ||
data['token'] = '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', | ||
return json_response(data) | ||
|
||
def fake_get_item_endpoint(self, uid): | ||
return json_response({ | ||
'uid': uid, | ||
'title': 'fake item', | ||
}) | ||
|
||
|
||
class CleanSlateDatabase(BehaviorDefinition): | ||
# Sure uses ``context_namespace`` internally to namespace the | ||
# self-assigned attributes into the context in order to prevent | ||
# attribute name collision | ||
context_namespace = 'db' | ||
|
||
def setup(self, sqlalchemy_database_uri='mysql://root@localhost/test-database'): | ||
self.engine = create_engine(sqlalchemy_database_uri) | ||
self.metadata = sqlalchemy_metadata | ||
# Dropping the whole schema just in case a previous test | ||
# execution fails and leaves the database dirty before having | ||
# the chance to run .teardown() | ||
self.metadata.drop_all(engine) | ||
self.metadata.create_all(engine) | ||
|
||
|
||
@apply_behavior(UseFakeHTTPAPIServer, http_port=5001) | ||
def test_with_real_network_io(context): | ||
response = requests.post('http://localhosst:5001/auth', data=json.dumps({'username': 'foobar'})) | ||
response.headers.should.have.key('Content-Type').being.equal('application/json') | ||
response.json().should.equal({ | ||
'token': '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', | ||
'username': 'foobar', | ||
}) |
119 changes: 119 additions & 0 deletions
119
examples/unit-tests/behavior_definition_simplify_mock.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# flake8: noqa | ||
# -*- coding: utf-8 -*- | ||
|
||
# =========================================== | ||
# Behavior definition examples in pseudo code | ||
# =========================================== | ||
|
||
|
||
#################################################################################### | ||
|
||
# UNIT TESTS | ||
# --------- | ||
|
||
# Goals: | ||
# ~~~~~ | ||
# | ||
# * Provide an API that leverages mocking ``sure.scenario.MockfulBehaviorDefinition`` | ||
# * Provide a didactic and fun way to keep the mocks organized | ||
# * Make monkey-patching of modules more fun: | ||
# * Always forward the mocked objects as keyword-arguments as | ||
# opposed to the default behavior of ``@patch()`` decorator stack | ||
# which uses positional arguments, polute test declarations and | ||
# demands menial, extrenuous work in case of test refactoring. | ||
# * ``self.mock.module(members={})`` is a shorthand for creating a dummy module object that contains the given members as properties | ||
|
||
# Notes: | ||
# ~~~~~ | ||
# | ||
# * the following methods require exactly one positional argument: ``mock_name``, otherwise: ``raise AssertionError("self.mock.simple() and self.mock.magic() require one positional argument: its name.")`` | ||
# * ``self.mock.simple()`` | ||
# * ``self.mock.magic()`` | ||
# * self.mock.install() forwards keyword-arguments to ``mock.patch``, for example: | ||
# * ``self.mock.install('myapp.api.http.db.engine.connect', return_value='DUMMY_CONNECTION') | ||
# * ``self.mock.install('myapp.api.http.db.engine.connect', return_value='DUMMY_CONNECTION') | ||
# * self.mock.install() always generates MagicMocks | ||
# * self.mock.install() accepts the parameter ``argname='some_identifier`` to be passed the keyword-argument that contains the ``Mock`` instance returned by ``.install()`` | ||
# * When ``argname=None`` the mock instance will be passed with a keyword-arg whose value is derived from the last __name__ found in the patch target, that is: ``self.mock.install('myapp.core.BaseServer')`` will change the test signature to: ``def test_something(context, BaseServer)``. | ||
# * self.mock.install() will automatically name the mock when the ``name=""`` keyword-arg is not provided | ||
# * self.mock.uninstall() accepts one positional argument: identified and then automatically match it against: | ||
# * self.scenario.forward_argument() allows for arbitrary parameters in the test function, that is: ``self.scenario.forward_argument(connection=self.connection)`` will change the test signature to: ``def test_something(context, connection)``. | ||
# * Developers do *NOT* need to manually uninstall mocks, but that is | ||
# still permitted to encompass the cases where it has to be | ||
# accomplished in mid-test, for example: | ||
# | ||
# @apply_behavior(FakeSettings) | ||
# @apply_behavior(PreventRedisIO) | ||
# def test_something(context): | ||
# # perform some action that requires a stubbed setting | ||
# context.redis.mock.uninstall(name=' | ||
# # peform | ||
# | ||
|
||
# <unit-test-example-pseudo-code> | ||
from mock import Mock | ||
from myapp import settings | ||
from myapp.api.http import APIServer | ||
from sure.scenario import MockfulBehaviorDefinition, apply_behavior, require_behavior | ||
|
||
|
||
class FakeSettings(MockfulBehaviorDefinition): | ||
"""automatically patches myapp.settings and overriding its keys with | ||
the provided kwargs.""" | ||
|
||
context_namespace = 'settings' | ||
|
||
def setup(self, **kwargs): | ||
# 1. make 2 copies: | ||
# * one original copy for backup | ||
# * one containing the overrides | ||
self.original_state = settings.to_dict() | ||
cloned_state = settings.to_dict() | ||
|
||
# 2. Apply the overrides in the cloned state | ||
cloned_state.update(kwargs) | ||
|
||
# 3. Create a module mock containing the members | ||
fake_settings = self.mock.module(members=fake_state) | ||
|
||
# 4. Install the mocked settings | ||
self.mock.install('myapp.api.http.settings', fake_settings) | ||
|
||
|
||
class PreventRedisIO(MockfulBehaviorDefinition): | ||
context_namespace = 'redis' | ||
|
||
def setup(self): | ||
self.connection = self.mock.simple('redis-connection-instance') | ||
self.mock.install('myapp.api.http.RedisConnection', argname='connection', return_value=self.connection) | ||
|
||
class PreventSQLIO(MockfulBehaviorDefinition): | ||
context_namespace = 'sql' | ||
|
||
def setup(self): | ||
self.engine = self.mock.simple('sqlalchemy.engine') | ||
self.connection = self.engine.return_value | ||
self.mock.install('myapp.api.http.sqlengine', return_value=self.engine) | ||
self.scenario.forward_argument(connection=self.connection) | ||
|
||
|
||
class IOSafeAPIServer(BehaviorGroup): | ||
layers = [ | ||
FakeSettings(SESSION_SECRET='dummy'), | ||
PreventRedisIO(), | ||
PreventSQLIO() | ||
] | ||
|
||
|
||
@apply_behavior(IOSafeAPIServer) | ||
def test_api_get_user(context, connection, ): | ||
('APIServer.handle_get_user() should try to retrieve from the redis cache first') | ||
|
||
# Given a server | ||
api = APIServer() | ||
|
||
# When I call .handle_get_user() | ||
response = api.handle_get_user(user_uuid='b1i6c00010ff1ceb00dab00b') | ||
|
||
# Then it should have returned a response | ||
response.should.be.a('flask.Response') |
123 changes: 123 additions & 0 deletions
123
examples/unit-tests/setup_and_teardown_with_behavior.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
import json | ||
import requests | ||
from gevent.pool import Pool | ||
from flask import Flask, Response | ||
from sqlalchemy import create_engine | ||
from sqlalchemy import MetaData | ||
|
||
from myapp.db import sqlalchemy_metadata | ||
from sure.scenario import BehaviorDefinition, apply_behavior | ||
|
||
|
||
# Changes in version 1.5.0 [draft] | ||
# ~~~~~~~~~~~~~~~~~~~~~~~~ | ||
# | ||
# * Introducing the concept of BehaviorDefinition: a clean and | ||
# decoupled way to reutilize setup/teardown behaviors. So instead of | ||
# the classic massive setup/teardown methods and/or chaotic | ||
# ``unittest.TestCase`` subclass inheritance every test can be | ||
# decorated with @apply_behavior(CustomBehaviorDefinitionTypeObject) | ||
# | ||
# * Avoid using the word "test" in your "Behavior Definitions" so that | ||
# nose will not mistake your BehaviorDefinition with an actual test | ||
# case class and thus execute .setup() and .teardown() in an | ||
# undesired manner. | ||
|
||
|
||
|
||
def get_json_request(self): | ||
"""parses the request body as JSON without running any sort of validation""" | ||
return json.loads(request.data) | ||
|
||
def json_response(response_body, status=200, headers=None): | ||
"""utility that automatically serializes the provided payload in JSON | ||
and generates :py:`flask.Response` with the ``application/json`` | ||
content-type. | ||
:param response_body: a python dictionary or any JSON-serializable python object. | ||
""" | ||
headers = headers or {} | ||
serialized = json.dumps(response_body, indent=2) | ||
headers[b'Content-Type'] = 'application/json' | ||
return Response(serialized, status=code, headers=headers) | ||
|
||
|
||
class GreenConcurrencyBehaviorDefinition(BehaviorDefinition): | ||
# NOTE: | ||
# ---- | ||
# | ||
# * Sure uses ``context_namespace`` internally to namespace the | ||
# self-assigned attributes into the context in order to prevent | ||
# attribute name collision. | ||
|
||
context_namespace = 'green' | ||
|
||
def setup(self, pool_size=1): | ||
self.pool = Pool(pool_size) | ||
|
||
|
||
class UseFakeHTTPAPIServer(GreenConcurrencyBehaviorDefinition): | ||
context_namespace = 'fake_api' | ||
|
||
def setup(self, http_port): | ||
# NOTES: | ||
# ~~~~~~ | ||
# | ||
# * GreenConcurrencyBehaviorDefinition.setup() is automatically called by | ||
# * sure in the correct order | ||
# | ||
# * Sure automatically takes care of performing top-down calls | ||
# to every parent of your behavior. | ||
# | ||
# * In simple words, this UseFakeHTTPAPIServer behavior will automatically call GreenConcurrencyBehaviorDefinition | ||
|
||
# 1. Create a simple Flask server | ||
self.server = Flask('fake.http.api.server') | ||
# 2. Setup fake routes | ||
self.server.add_url_rule('/auth', view_func=self.fake_auth_endpoint) | ||
self.server.add_url_rule('/item/<uid>', view_func=self.fake_get_item_endpoint) | ||
# 3. Run the server | ||
self.pool.spawn(self.server.run, port=http_port) | ||
|
||
def teardown(self): | ||
self.server.stop() | ||
|
||
def fake_auth_endpoint(self): | ||
data = get_json_request() | ||
data['token'] = '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', | ||
return json_response(data) | ||
|
||
def fake_get_item_endpoint(self, uid): | ||
return json_response({ | ||
'uid': uid, | ||
'title': 'fake item', | ||
}) | ||
|
||
|
||
class CleanSlateDatabase(BehaviorDefinition): | ||
# Sure uses ``context_namespace`` internally to namespace the | ||
# self-assigned attributes into the context in order to prevent | ||
# attribute name collision | ||
context_namespace = 'db' | ||
|
||
def setup(self, sqlalchemy_database_uri='mysql://root@localhost/test-database'): | ||
self.engine = create_engine(sqlalchemy_database_uri) | ||
self.metadata = sqlalchemy_metadata | ||
# Dropping the whole schema just in case a previous test | ||
# execution fails and leaves the database dirty before having | ||
# the chance to run .teardown() | ||
self.metadata.drop_all(engine) | ||
self.metadata.create_all(engine) | ||
|
||
|
||
@apply_behavior(UseFakeHTTPAPIServer, http_port=5001) | ||
def test_with_real_network_io(context): | ||
response = requests.post('http://localhosst:5001/auth', data=json.dumps({'username': 'foobar'})) | ||
response.headers.should.have.key('Content-Type').being.equal('application/json') | ||
response.json().should.equal({ | ||
'token': '$2b$12$heKIpWg0wJQ6BeKxPSJCP.7Vf9hn8s6yFs8yGWnWdPZ48toXbjK9W', | ||
'username': 'foobar', | ||
}) |
Oops, something went wrong.