Python Nose2 Tests Not Finishing When Class Method Called - python

When I run my tests that include calling a #classmethod using setuptools and nose2, the testing suite doesn't finish it just keeps on running. However I have checked that the test does indeed pass and reach the end of the function, the test suite just doesn't finish running. If I remove the tests using decode_auth_token it works fine. I was able to narrow it done to the class methods because I tested other class methods as well and they causes the same problem
Does anyone have any idea why this might be happening? Below are the relevant pieces of code without posting too much of my code
Code in my User Model
#classmethod
def decode_auth_token(cls, auth_token):
try:
payload = jwt.decode(auth_token, config.SECRET_KEY, algorithms=['HS256'])
# check the hash of what we expect the token to be and token we got to be the same
if bcrypt.check_password_hash(User.by_id(payload['sub']).api_token_hash, auth_token):
return payload['sub']
else:
return 'Token does not match Api Token.'
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid Token. Please log in again.'
The following two functions also cause the problem when called
#classmethod
def is_username_taken(cls, username):
return db.session.query(db.exists().where(User.username==username)).scalar()
#classmethod
def is_email_taken(cls, email):
return db.session.query(db.exists().where(User.email==email)).scalar()
This function does not cause the problem when called though
#classmethod
def by_username(cls, username):
return User.query.filter(User.username == username).first()
Here are the tests
import unittest
import sys
from . import AppTestCase, API_ROOT
from app.extensions import db, bcrypt
from app.models import User, UserSchema, Location, Company
class TestUserModel(AppTestCase):
def test_encode_auth_token(self):
user = User.by_username('jdoe')
auth_token = user.encode_auth_token(user.id)
self.assertTrue(isinstance(auth_token, bytes))
def test_decode_auth_token(self):
user = User.by_username('jdoe')
auth_token = user.encode_auth_token(user.id)
self.assertTrue(isinstance(auth_token, bytes))
self.assertEqual(User.decode_auth_token(auth_token), user.id)
print('DONE')
The first test works fine, the second test prints out Done and properly decodes the auth_token returning the proper user id but does not cause the test suite to finish. It just keeps running after printing done.
And here is the setup script, I run the tests using python setup.py test
import os
from setuptools import setup, find_packages, Command
# Thanks http://stackoverflow.com/questions/3779915/why-does-python-setup-py-sdist-create-unwanted-project-egg-info-in-project-r
class CleanCommand(Command):
"""Custom clean command to tidy up the project root."""
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
os.system('rm -vrf ./build ./dist ./*.pyc ./*.tgz ./*.egg-info')
with open('requirements.txt') as f:
requirements = f.read().splitlines()
setup(
name="XXX",
description="XXX",
version=1.0,
packages=find_packages(),
install_requires=requirements,
include_package_data=True,
test_suite='nose2.collector.collector',
tests_require=['nose2'],
cmdclass={
'clean': CleanCommand,
}
)
Output when Running and Not Stopping
running test
Searching for nose2
Best match: nose2 0.6.5
Processing nose2-0.6.5-py3.6.egg
Using XXX/.eggs/nose2-0.6.5-py3.6.egg
running egg_info
writing doomfist.egg-info/PKG-INFO
writing dependency_links to XXX.egg-info/dependency_links.txt
writing requirements to XXX.egg-info/requires.txt
writing top-level names to XXX.egg-info/top_level.txt
reading manifest file 'XXX.egg-info/SOURCES.txt'
writing manifest file 'XXX.egg-info/SOURCES.txt'
running build_ext
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/parser.py:50: DeprecationWarning: invalid escape sequence \.
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/parser.py:50: DeprecationWarning: invalid escape sequence \.
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/tz/win.py:197: DeprecationWarning: invalid escape sequence \{
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/tz/win.py:247: DeprecationWarning: invalid escape sequence \{
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/tz/win.py:197: DeprecationWarning: invalid escape sequence \{
/Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/python_dateutil-2.6.0-py3.6.egg/dateutil/tz/win.py:247: DeprecationWarning: invalid escape sequence \{
NOT running in debug mode
DONE
^]^\[1] 35752 quit python setup.py test
EDIT----- Sorry for the Huge post now
With someone's advice in the comments I used a debugger to determine it does indeed finish the tests. And where it actually get's stuck is during tearDown(). The following function of mine is where it gets stuck.
def tearDown(self):
"""Clean db session and drop all tables."""
db.drop_all()
Following the debugger farther down I determined it ultimately gets stuck
for table, fkcs in collection:
if table is not None:
self.traverse_single(table, drop_ok=True, _is_metadata_operation=True)
else:
for fkc in fkcs:
...
More specifically on this method self.traverse_single(table, drop_ok=True, _is_metadata_operation=True). I'm assuming it gets stuck waiting for the generator to return? Unsure but below is the last lines I got before it gets stuck again.
> /Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/SQLAlchemy-1.1.11-py3.6-macosx-10.7-x86_64.egg/sqlalchemy/sql/ddl.py(929)visit_table()->None
-> _is_metadata_operation=_is_metadata_operation)
(Pdb) n
--Call--
> /Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/SQLAlchemy-1.1.11-py3.6-macosx-10.7-x86_64.egg/sqlalchemy/sql/visitors.py(150)_visitor_iterator()-><sqlalchemy.s...t 0x112045630>
-> yield v
(Pdb) n
GeneratorExit
> /Users/XXX/anaconda3/envs/XXX/lib/python3.6/site-packages/SQLAlchemy-1.1.11-py3.6-macosx-10.7-x86_64.egg/sqlalchemy/sql/visitors.py(150)_visitor_iterator()-><sqlalchemy.s...t 0x112045630>
-> yield v
(Pdb) l
145 def _visitor_iterator(self):
146 """iterate through this visitor and each 'chained' visitor."""
147
148 v = self
149 while v:
150 -> yield v
151 v = getattr(v, '_next', None)
152
153 def chain(self, visitor):
154 """'chain' an additional ClauseVisitor onto this ClauseVisitor.
155
(Pdb) n
I believe it gets stuck on the following table of mine
from ..helpers import get_current_time
from ..extensions import db, ma
from ..constants import STRING_LEN, DESCRIPTION_LEN
from .worker import WorkerSchema
class Injury(db.Model):
__tablename__ = "injuries"
def __repr__(self):
return '<Injury %r>' % (self.id)
id = db.Column(db.Integer, primary_key = True)
title = db.Column(db.String(STRING_LEN), nullable=False)
description = db.Column(db.String(DESCRIPTION_LEN), nullable=False)
worker_id = db.Column(db.Integer, db.ForeignKey('workers.id'))
created_at = db.Column(db.DateTime, nullable=False, default = get_current_time)
updated_at = db.Column(db.DateTime, nullable=False, default = get_current_time, onupdate=get_current_time)
# Relationships
worker = db.relationship('Worker', back_populates='injuries')
# ================================================================
# ================================================================
# methods
# ================================================================
# Class methods
#classmethod
def by_id(cls, id):
return cls.query.filter(Injury.id==id).first()
class InjurySchema(ma.Schema):
class Meta:
fields = ('id', 'title', 'description', 'worker')
worker = ma.Nested(WorkerSchema)

I was able to get it to work by adding db.session.close() before my drop_all command based on this post SQLAlchemy blocked on dropping tables
def tearDown(self):
"""Clean db session and drop all tables."""
db.session.close()
db.drop_all()
I still need to find why the session is open and where I need to close it though

Related

How can I provide a non-fixture pytest parameter via a custom decorator?

We have unit tests running via Pytest, which use a custom decorator to start up a context-managed mock echo server before each test, and provide its address to the test as an extra parameter. This works on Python 2.
However, if we try to run them on Python 3, then Pytest complains that it can't find a fixture matching the name of the extra parameter, and the tests fail.
Our tests look similar to this:
#with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, url):
res_id = self._test_resource(url)['id']
result = update_resource(None, res_id)
assert not result, result
self.assert_archival_error('Server reported status error: 404 Not Found', res_id)
With a decorator function like this:
from functools import wraps
def with_mock_url(url=''):
"""
Start a MockEchoTestServer and call the decorated function with the server's address prepended to ``url``.
"""
def decorator(func):
#wraps(func)
def decorated(*args, **kwargs):
with MockEchoTestServer().serve() as serveraddr:
return func(*(args + ('%s/%s' % (serveraddr, url),)), **kwargs)
return decorated
return decorator
On Python 2 this works; the mock server starts, the test gets a URL similar to "http://localhost:1234/?status=404&content=test&content-type=csv", and then the mock is shut down afterward.
On Python 3, however, we get an error, "fixture 'url' not found".
Is there perhaps a way to tell Python, "This parameter is supplied from elsewhere and doesn't need a fixture"? Or is there, perhaps, an easy way to turn this into a fixture?
You can use url as args parameter
#with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, *url):
url[0] # the test url
Looks like Pytest is content to ignore it if I add a default value for the injected parameter, to make it non-mandatory:
#with_mock_url('?status=404&content=test&content-type=csv')
def test_file_not_found(self, url=None):
The decorator can then inject the value as intended.
consider separating the address from the service of the url. Using marks and changing fixture behavior based on the presence of said marks is clear enough. Mock should not really involve any communication, but if you must start some service, then make it separate from
with_mock_url = pytest.mark.mock_url('http://www.darknet.go')
#pytest.fixture
def url(request):
marker = request.get_closest_marker('mock_url')
if marker:
earl = marker.args[0] if args else marker.kwargs['fake']
if earl:
return earl
try:
#
earl = request.param
except AttributeError:
earl = None
return earl
#fixture
def server(request):
marker = request.get_closest_marker('mock_url')
if marker:
# start fake_server
#with_mock_url
def test_resolve(url, server):
server.request(url)

How to mock a class method that is called from another class with pytest_mock

In the below files I have
InternalDogWebhookResource which calls VisitOrchestrator.fetch_visit. I am attempting to write a test for InternalDogWebhookResource but mock VisitOrchestrator.fetch_visit since it is a network call.
I have tried the mock paths:
api.dog.handlers.internal.VisitOrchestrator.fetch_visit
api.dog.handlers.internal.InternalDogWebhookResource.VisitOrchestrator.fetch_visit
api.dog.handlers.internal.InternalDogWebhookResource.fetch_visit
and many others, but I am always getting AssertionError: assert None
I can confirm that the client.post in the test works because when i remove the mock asserts, i get a response back from the api which means fetch_visit is called.
How can I find the mocker.patch path?
api/dog/handlers/internal.py
from api.dog.helpers.visits import VisitOrchestrator
#api.route("/internal/dog/webhook")
class InternalDogWebhookResource():
def post(self) -> JsonResponse:
if event_type == EventType.CHANGE:
VisitOrchestrator.fetch_visit(event['visitId'])
return JsonResponse(status=204)
api/dog/helpers/visits.py
class VisitOrchestrator:
#classmethod
def fetch_visit(cls, visit_id: str) -> VisitModel:
# do stuff
return visit
tests/v0/dog/handlers/test_webhook.py
import pytest
from pytest_mock import MockerFixture
from api.dog.handlers.internal import InternalDogWebhookResource, EventType
from tests.v0.utils import url_for
def test_webhook_valid(client, config, mocker: MockerFixture):
visit_id = '1231231'
mock_object = mocker.patch(
'api.dog.handlers.internal.VisitOrchestrator.fetch_visit',
return_value=visit_id,
)
res = client.post(
url_for(InternalDogWebhookResource),
json={'blag': 'blargh'}
)
assert mock_object.assert_called_once()
You're doing the right things - your second approach is generally the way to go with mocks (mocking api.dog.handlers.internal.InternalDogWebhookResource.VisitOrchestrator.fetch_visit)
I would try to do the minimal test code function:
def test_webhook_valid(mocker):
mock_fetch_visit = mocker.MagicMock(name='fetch_visit')
mocker.patch('api.dog.handlers.internal.VisitOrchestrator.fetch_visit',
new=mock_fetch_visit)
InternalDogWebhookResource().post()
assert 1 == mock_fetch_visit.call_count
If this works for you - maybe the problem is with the client or other settings in your test method.

Python pytest pytest_exception_interact customize exception information from VCR.py exception

Context
I have started using pytest-vcr which is a pytest plugin wrapping VCR.py which I have documented in this blog post on Advanced Python Testing.
It records all HTTP traffic to cassettes/*.yml files on the first test run to save snapshots. Similar to Jest snapshot testing for web components.
On subsequent test runs, if a request is malformed, it won't find a match and throws an exception saying that recording new requests is forbidden and it did not find an existing recording.
Question
VCR.py raises a CannotOverwriteExistingCassetteException which is not particularly informative as to why it didn't match.
How do I leverage pytest pytest_exception_interact hooks to replace this exception with a more informative one leveraging fixture information?
I dove into my site-packages where VCR.py is pip installed and rewrote how I want it to handle the exception. I just need to know how to get this pytest_exception_interact hook to work correctly to access the fixtures from that test node (before it gets cleaned up) and raise a different exception.
Example
Lets get the dependencies.
$ pip install pytest pytest-vcr requests
test_example.py:
import pytest
import requests
#pytest.mark.vcr
def test_example():
r = requests.get("https://www.stackoverflow.com")
assert r.status_code == 200
$ pytest test_example.py --vcr-record=once
...
test_example.py::test_example PASSED
...
$ ls cassettes/
cassettes/test_example.yml
$ head cassettes/test_example.yml
interactions:
- request:
uri: https://wwwstackoverflow.com
body: null
headers:
Accept:
- '*/*'
$ pytest test_example.py --vcr-record=none
...
test_example.py::test_example PASSED
...
Now change the URI in the test to "https://www.google.com":
test_example.py:
import pytest
import requests
#pytest.mark.vcr
def test_example():
r = requests.get("https://www.google.com")
assert r.status_code == 200
And run the test again to detect the regression:
$ pytest test_example.py --vcr-record=none
E vcr.errors.CannotOverwriteExistingCassetteException: No match for the request (<Request (GET) https://www.google.com/>)
...
I can add a conftest.py file to the root of my test structure to create a local plugin, and I can verify that I can intercept the exception and inject my own using:
conftest.py
import pytest
from vcr.errors import CannotOverwriteExistingCassetteException
from vcr.config import VCR
from vcr.cassette import Cassette
class RequestNotFoundCassetteException(CannotOverwriteExistingCassetteException):
...
#pytest.fixture(autouse=True)
def _vcr_marker(request):
marker = request.node.get_closest_marker("vcr")
if marker:
cassette = request.getfixturevalue("vcr_cassette")
vcr = request.getfixturevalue("vcr")
request.node.__vcr_fixtures = dict(vcr_cassette=cassette, vcr=vcr)
yield
#pytest.hookimpl(hookwrapper=True)
def pytest_exception_interact(node, call, report):
excinfo = call.excinfo
if report.when == "call" and isinstance(excinfo.value, CannotOverwriteExistingCassetteException):
# Safely check for fixture pass through on this node
cassette = None
vcr = None
if hasattr(node, "__vcr_fixtures"):
for fixture_name, fx in node.__vcr_fixtures.items():
vcr = fx if isinstance(fx, VCR)
cassette = fx if isinstance(fx, Cassette)
# If we have the extra fixture context available...
if cassette and vcr:
match_properties = [f.__name__ for f in cassette._match_on]
cassette_reqs = cassette.requests
# filtered_req = cassette.filter_request(vcr._vcr_request)
# this_req, req_str = __format_near_match(filtered_req, cassette_reqs, match_properties)
# Raise and catch a new excpetion FROM existing one to keep the traceback
# https://stackoverflow.com/a/24752607/622276
# https://docs.python.org/3/library/exceptions.html#built-in-exceptions
try:
raise RequestNotFoundCassetteException(
f"\nMatching Properties: {match_properties}\n" f"Cassette Requests: {cassette_reqs}\n"
) from excinfo.value
except RequestNotFoundCassetteException as e:
excinfo._excinfo = (type(e), e)
report.longrepr = node.repr_failure(excinfo)
This is the part where the documentation on the internet gets pretty thin.
How do I access the vcr_cassette fixture and return a different exception?
What I want to do is get the filtered_request that was attempting to be requested and the list of cassette_requests and using the Python difflib standard library produce deltas against the information that diverged.
PyTest Code Spelunking
The internals of running a single test with pytest triggers pytest_runtest_protocol which effectively runs the following three call_and_report calls to get a collection of reports.
src/_pytest/runner.py:L77-L94
def runtestprotocol(item, log=True, nextitem=None):
# Abbreviated
reports = []
reports.append(call_and_report(item, "setup", log))
reports.append(call_and_report(item, "call", log))
reports.append(call_and_report(item, "teardown", log))
return reports
So I'm after modifying the report at the call stage... but still no clue how I get access to the fixture information.
src/_pytest/runner.py:L166-L174
def call_and_report(item, when, log=True, **kwds):
call = call_runtest_hook(item, when, **kwds)
hook = item.ihook
report = hook.pytest_runtest_makereport(item=item, call=call)
if log:
hook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
hook.pytest_exception_interact(node=item, call=call, report=report)
return report
It looks like there are some helper methods for generating a new ExceptionRepresentation so I updated the conftest.py example.
src/_pytest/reports.py:L361
longrepr = item.repr_failure(excinfo)
UPDATE #1 2019-06-26: Thanks to some pointers from #hoefling in the comments I updated my conftest.py.
Correctly re-raising the exception using the raise ... from ... form.
Override the _vcr_marker to attach the vcr and vcr_cassette fixtures to the request.node which represent that individual test item.
Remaining: Get access to the intercepted request from the patched VCRConnection...
UPDATE #2 2019-06-26
It would seem impossible to get at the VCRHTTPConnections that were patched in creating the cassette context manager. I have opened up the following pull request to pass as arguments when the exception is thrown, to then catch and handle arbitrarily down stream.
https://github.com/kevin1024/vcrpy/pull/445
Related
Related questions that are informative but still don't answer this question.
Customizing error message for specific exceptions in pytest
Thanks to comments and guidance in the comments from #hoefling.
I could attach the cassette fixture to the request.node in a conftest.py local plugin overriding the pytest-vcr marker...
#pytest.fixture(autouse=True)
def _vcr_marker(request):
marker = request.node.get_closest_marker("vcr")
if marker:
cassette = request.getfixturevalue("vcr_cassette")
vcr = request.getfixturevalue("vcr")
request.node.__vcr_fixtures = dict(vcr_cassette=cassette, vcr=vcr)
yield
But I needed more than the cassette to get to my solution.
Ingredients
Use the pytest_exception_interact hook
Pull request to VCR.py https://github.com/kevin1024/vcrpy/pull/446
PR #439 and PR #441 by arthurHamon2 has been huge to fix the test suite and also integrate matcher differencing outputs.
BONUS: Use the raise ... from ... form of throwing an exception
Recipe
Get latest VCRpy
These patches were released in vcrpy v2.1.0
pip install vcrpy==2.1.0
Override the pytest_exception_interact hook
In the root of your test directory create a conftest.py to create a local plugin that overrides the pytest_exception_interact hook.
#pytest.hookimpl(hookwrapper=True)
def pytest_exception_interact(node, call, report):
"""Intercept specific exceptions from tests."""
if report.when == "call" and isinstance(call.excinfo.value, CannotOverwriteExistingCassetteException):
__handle_cassette_exception(node, call, report)
yield
Extract the Cassette and the Request from the exception.
# Define new exception to throw
class RequestNotFoundCassetteException(Exception):
...
def __handle_cassette_exception(node, call, report):
# Safely check for attributes attached to exception
vcr_request = None
cassette = None
if hasattr(call.excinfo.value, "cassette"):
cassette = call.excinfo.value.cassette
if hasattr(call.excinfo.value, "failed_request"):
vcr_request = call.excinfo.value.failed_request
# If we have the extra context available...
if cassette and vcr_request:
match_properties = [f.__name__ for f in cassette._match_on]
this_req, req_str = __format_near_match(cassette.requests, vcr_request, match_properties)
try:
raise RequestNotFoundCassetteException(f"{this_req}\n\n{req_str}\n") from call.excinfo.value
except RequestNotFoundCassetteException as e:
call.excinfo._excinfo = (type(e), e)
report.longrepr = node.repr_failure(call.excinfo)

How do I confirm entities are saved with GAE's Eventual Consistency?

I'm trying to create tests to verify that my entities are being saved in the database.
When I put breakpoints in the post function, I can see that the customer count changes after the record is saved.
I read https://cloud.google.com/appengine/docs/python/tools/localunittesting#Python_Writing_High_Replication_Datastore_tests
From what I understood, the tests were failing because of Eventual Consistency and the way to get around that was to change the PseudoRandomHRConsistencyPolicy settings.
policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(probability=1)
And when I ran the test again I got the same error.
What am I doing wrong with creating these tests?
> /Users/Bryan/work/GoogleAppEngine/dermalfillersecrets/main.py(137)post()
-> customer.put()
(Pdb) l
134 query = Customer.query()
135 orig_customer_count = query.count()
136 import pdb; pdb.set_trace()
137 -> customer.put()
138 import pdb; pdb.set_trace()
139 query_params = {'leadbook_name': leadbook_name}
140 self.redirect('/?' + urllib.urlencode(query_params))
141
142 config = {}
(Pdb) orig_customer_count
5
(Pdb) c
> /Users/Bryan/work/GoogleAppEngine/dermalfillersecrets/main.py(139)post()
-> query_params = {'leadbook_name': leadbook_name}
(Pdb) l
134 query = Customer.query()
135 orig_customer_count = query.count()
136 import pdb; pdb.set_trace()
137 customer.put()
138 import pdb; pdb.set_trace()
139 -> query_params = {'leadbook_name': leadbook_name}
140 self.redirect('/?' + urllib.urlencode(query_params))
141
142 config = {}
143 config['webapp2_extras.sessions'] = {
144 'secret_key': 'my-super-secret-key',
(Pdb) query.count()
6
The entities also show up in the Datastore Viewer.
However, my test keeps failing.
$ nosetests --with-gae
F
======================================================================
FAIL: test_guest_can_submit_contact_info (dermalfillersecrets.functional_tests.NewVisitorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/Bryan/work/GoogleAppEngine/dermalfillersecrets/functional_tests.py", line 80, in test_guest_can_submit_contact_info
self.assertNotEqual(orig_custs, query.count())
AssertionError: 0 == 0
This is the functional_tests.py file contents:
import os, sys
sys.path.append("/usr/local/google_appengine")
sys.path.append("/usr/local/google_appengine/lib/yaml/lib")
sys.path.append("/usr/local/google_appengine/lib/webapp2-2.5.2")
sys.path.append("/usr/local/google_appengine/lib/django-1.5")
sys.path.append("/usr/local/google_appengine/lib/cherrypy")
sys.path.append("/usr/local/google_appengine/lib/concurrent")
sys.path.append("/usr/local/google_appengine/lib/docker")
sys.path.append("/usr/local/google_appengine/lib/requests")
sys.path.append("/usr/local/google_appengine/lib/websocket")
sys.path.append("/usr/local/google_appengine/lib/fancy_urllib")
sys.path.append("/usr/local/google_appengine/lib/antlr3")
import unittest
from selenium import webdriver
from google.appengine.api import memcache
from google.appengine.ext import db
from google.appengine.ext import testbed
import dev_appserver
from google.appengine.tools.devappserver2 import devappserver2
class NewVisitorTest(unittest.TestCase):
def setUp(self):
self.testbed = testbed.Testbed()
self.testbed.activate()
#self.testbed.setup_env(app_id='dermalfillersecrets')
self.testbed.init_user_stub()
####################################################
# this sets testbed to imitate strong consistency
from google.appengine.datastore import datastore_stub_util
policy = datastore_stub_util.PseudoRandomHRConsistencyPolicy(probability=1)
self.testbed.init_datastore_v3_stub(consistency_policy=policy)
self.testbed.init_memcache_stub()
####################################################
# setup the dev_appserver
APP_CONFIGS = ['app.yaml']
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3)
def tearDown(self):
self.browser.quit()
self.testbed.deactivate()
def test_guest_can_submit_contact_info(self):
from main import Customer
query = Customer.query()
orig_custs = query.count()
self.browser.get('http://localhost:8080')
self.browser.find_element_by_name('id_name').send_keys("Kallie Wheelock")
self.browser.find_element_by_name('id_street').send_keys("123 main st")
self.browser.find_element_by_name('id_phone').send_keys('(404)555-1212')
self.browser.find_element_by_name('id_zip').send_keys("30306")
self.browser.find_element_by_name('submit').submit()
# this should return 1 more record
#import pdb; pdb.set_trace()
query = Customer.query()
self.assertNotEqual(orig_custs, query.count())
assert(Customer.query(Customer.name == "Kallie Wheelock").get())
# Delete the Customer record
Customer.query(Customer.name =="Kallie Wheelock").delete()
The PseudoRandomHRConsistencyPolicy is not helping you here because your selenium test is submitting a live html form and the subsequent db update happening on the server which is outside scope of your policy.
What you testing here is the end to end testing not the unit test per se. So your selenium test should take care of the real world scenario and should wait for a predefined period of time before comparing the counts.
There's nothing wrong with strong/eventual consistency, but the design of your tests is wrong. Why you're trying to deal with devappserver in your tests by yourself? Why you're trying to remove entities in the end of the test? Each test should be isolated from each other and start from empty datastore with some possible initializations.
Please, use the latest version of NoseGAE plugin. Here's two simple tests about strong/eventual consistency:
import unittest
from google.appengine.ext import ndb
from google.appengine.datastore import datastore_stub_util
class Foo(ndb.Model):
pass
class TestEventualConsistency(unittest.TestCase):
nosegae_datastore_v3 = True
nosegae_datastore_v3_kwargs = {
'consistency_policy': datastore_stub_util.PseudoRandomHRConsistencyPolicy(
probability=0)}
def test_eventual_consistency(self):
self.assertEqual(Foo.query().count(), 0)
Foo().put()
self.assertEqual(Foo.query().count(), 0)
class TestStrongConsistency(unittest.TestCase):
nosegae_datastore_v3 = True
nosegae_datastore_v3_kwargs = {
'consistency_policy': datastore_stub_util.PseudoRandomHRConsistencyPolicy(
probability=1)}
def test_strong_consistency(self):
self.assertEqual(Foo.query().count(), 0)
Foo().put()
self.assertEqual(Foo.query().count(), 1)
Notice that I don't have anything about GAE paths, dev_appserver, etc.
You still can control testbed by yourself, but better configure it with nosegae_*. (read about this in plugin documentation)
And as I remember, it will work even if you will programmatically fill your HTML form, but its not unittests anymore though.
Try using ancestor queries to get strong consistency instead of eventual consistency. From the docs:
Ancestor queries allow you to make strongly consistent queries to the datastore...
If this does not work, the next thing I would try is to not reuse the query object but create a new the second time.
If this does not work either, my guess is that something else is wrong. I am not familiar with browser test, but I have used webtest with great success for testing web endpoints and have not had any consistency issues while unit testing.
Queries are eventually consistent (unless an ancestor is set), but a get operation is always consistent.
If your objective is to simply test the code for writing an entity, you can insert an entity in this test and check if you can retrieve this entity using its key.

there must be a better way to do this

This is an ugly, high maintenance factory. I really just need a way to use the string to instantiate an object with a name that matches the string. I think metaclass is the answer but I can't figure out how to apply it:
from commands.shVersionCmd import shVersionCmd
from commands.shVRFCmd import shVRFCmd
def CommandFactory(commandnode):
if commandnode.attrib['name'] == 'shVersionCmd': return shVersionCmd(commandnode)
if commandnode.attrib['name'] == 'shVRFCmd': return shVRFCmd(commandnode)
You can look up global names with the globals() function, which returns a dict:
from commands.shVersionCmd import shVersionCmd
from commands.shVRFCmd import shVRFCmd
# An explicit list of allowed commands to prevent malicious activity.
commands = ['shVersionCmd', 'shVRFCmd']
def CommandFactory(commandnode):
cmd = commandnode.attrib['name']
if cmd in commands:
fn = globals()[cmd]
fn(commandnode)
This answer How to make an anonymous function in Python without Christening it? discusses how to cleanly call blocks of code based on a key
eval is your friend:
from commands import *
def CommandFactory(commandnode):
name=commandnode.attrib['name']
assert name in ( "shVersionCmd", "shVRFCmd" ), "illegal command"
return eval(name+"."+name)(commandnode)
Note that if you are sure that name will never contain any illegal commands, you could remove the assert and turn the function into a no-maintenance-delight. In case of doubt, leave it in and maintain the list in a single place.
My personal preference would be to turn the dependencies between the factory and the command implementations around, so that each command registers itself with the factory.
Example implementation:
File commands/__init__.py:
import pkgutil
import commands
_commands = {}
def command(commandCls):
_commands[commandCls.__name__] = commandCls
return commandCls
def CommandFactory(commandnode):
name = commandnode.attrib['name']
if name in _commands.keys():
return _commands[name](commandnode)
# Load all commands
for loader, module_name, is_pkg in pkgutil.walk_packages(commands.__path__):
if module_name!=__name__:
module = loader.find_module(module_name).load_module(module_name)
File commands/mycommand.py:
from commands import command
#command
class MyCommand(object):
def __init__(self, commandnode):
pass
Small test:
from commands import CommandFactory
# Stub node implementation
class Node(object):
def __init__(self, name):
self.attrib = { "name": name }
if __name__=='__main__':
cmd = CommandFactory(Node("MyCommand"))
assert cmd.__class__.__name__=="MyCommand", "New command is instance of MyCommand"
cmd = CommandFactory(Node("UnknownCommand"))
assert cmd is None, "Returns None for unknown command type"

Categories

Resources