diff --git a/Flask_Testing.egg-info/PKG-INFO b/Flask_Testing.egg-info/PKG-INFO index e81d7f0..b536523 100644 --- a/Flask_Testing.egg-info/PKG-INFO +++ b/Flask_Testing.egg-info/PKG-INFO @@ -1,31 +1,31 @@ Metadata-Version: 1.1 Name: Flask-Testing -Version: 0.4.2 +Version: 0.7.1 Summary: Unit testing for Flask Home-page: https://github.com/jarus/flask-testing Author: Dan Jacob Author-email: danjac354@gmail.com License: BSD Description: Flask-Testing ------------- Flask unittest integration. Links ````` * `documentation ` * `development version ` Platform: any Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/Flask_Testing.egg-info/requires.txt b/Flask_Testing.egg-info/requires.txt index 2077213..e3e9a71 100644 --- a/Flask_Testing.egg-info/requires.txt +++ b/Flask_Testing.egg-info/requires.txt @@ -1 +1 @@ -Flask \ No newline at end of file +Flask diff --git a/PKG-INFO b/PKG-INFO index e81d7f0..b536523 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,31 +1,31 @@ Metadata-Version: 1.1 Name: Flask-Testing -Version: 0.4.2 +Version: 0.7.1 Summary: Unit testing for Flask Home-page: https://github.com/jarus/flask-testing Author: Dan Jacob Author-email: danjac354@gmail.com License: BSD Description: Flask-Testing ------------- Flask unittest integration. Links ````` * `documentation ` * `development version ` Platform: any Classifier: Development Status :: 4 - Beta Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules diff --git a/README b/README index 1e73046..c61c01d 100644 --- a/README +++ b/README @@ -1,7 +1,7 @@ -Flask-Testing +flask-testing Unit testing support for Flask. For full information please refer to the online docs: -http://packages.python.org/Flask-Testing +https://flask-testing.readthedocs.io/en/latest/ diff --git a/debian/changelog b/debian/changelog index 56c1866..127de0e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,15 +1,23 @@ -flask-testing (0.4.2-2) UNRELEASED; urgency=medium +flask-testing (0.7.1-1) unstable; urgency=medium + [ Ondřej Nový ] * d/control: Set Vcs-* to salsa.debian.org * d/copyright: Use https protocol in Format field * d/control: Deprecating priority extra as per policy 4.0.1 * Convert git repository from git-dpm to gbp layout * Use 'python3 -m sphinx' instead of sphinx-build for building docs - -- Ondřej Nový Tue, 13 Feb 2018 10:16:21 +0100 + [ Jonathan Carter ] + * Team upload (DPMT) + * New upstream release + * Upgrade to debhelper-compat (= 12) + * Update standards version to 4.3.0 + * Remove python2 version of package + + -- Jonathan Carter Tue, 26 Feb 2019 17:44:55 +0000 flask-testing (0.4.2-1) unstable; urgency=low * Initial release (Closes: #807054). -- Nicolas Dandrimont Wed, 20 Apr 2016 13:30:34 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index ec63514..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 diff --git a/debian/control b/debian/control index fecc634..4d496c7 100644 --- a/debian/control +++ b/debian/control @@ -1,54 +1,39 @@ Source: flask-testing Section: python Priority: optional Maintainer: Debian Python Modules Team -Uploaders: Stefano Zacchiroli , Nicolas Dandrimont -Build-Depends: debhelper (>= 9), dh-python, - python-all, - python-blinker, - python-flask, - python-setuptools, - python-twill, +Uploaders: Stefano Zacchiroli , + Nicolas Dandrimont +Build-Depends: debhelper-compat (= 12), + dh-python, python3-all, python3-blinker, python3-flask, python3-setuptools, python3-sphinx, -Standards-Version: 3.9.8 +Standards-Version: 4.3.0 Homepage: http://pythonhosted.org/Flask-Testing/ Vcs-Git: https://salsa.debian.org/python-team/modules/flask-testing.git Vcs-Browser: https://salsa.debian.org/python-team/modules/flask-testing -Package: python-flask-testing -Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, -Recommends: ${python:Recommends} -Suggests: ${python:Suggests} -XB-Python-Egg-Name: Flask-Testing -Description: unit testing utilities for the Flask micro web framework - Python 2.X - Flask-Testing is an extension for the Flask micro web framework that provides - unit testing helpers for Flask-based web applications. - . - This package contains the Python 2 modules for flask-testing - Package: python3-flask-testing Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, +Depends: ${misc:Depends}, ${python3:Depends} Recommends: ${python3:Recommends} Suggests: ${python3:Suggests} XB-Python-Egg-Name: Flask-Testing Description: unit testing utilities for the Flask micro web framework Flask-Testing is an extension for the Flask micro web framework that provides unit testing helpers for Flask-based web applications. . This package contains the Python 3 modules for flask-testing Package: python-flask-testing-doc Section: doc Architecture: all Depends: ${misc:Depends}, ${sphinxdoc:Depends} Description: documentation for the Flask-Testing Python library - documentation Flask-Testing is an extension for the Flask micro web framework that provides unit testing helpers for Flask-based web applications. . This package contains the sphinx-generated API documentation diff --git a/debian/copyright b/debian/copyright index b9d6485..84a418c 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,43 +1,44 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Flask-Testing Upstream-Contact: Dan Jacob Source: https://github.com/jarus/flask-testing Files: * Copyright: (c) 2010 by Dan Jacob. License: BSD-2-clause Files: debian/* -Copyright: 2015 © Stefano Zacchiroli +Copyright: 2015 Stefano Zacchiroli + 2019 Jonathan Carter License: BSD-2-clause License: BSD-2-clause Some rights reserved. . Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: . * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. . * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. . * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. . THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/debian/rules b/debian/rules index c157d0d..0636f46 100755 --- a/debian/rules +++ b/debian/rules @@ -1,12 +1,12 @@ #! /usr/bin/make -f export PYBUILD_NAME=flask-testing %: - dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild + dh $@ --with python3,sphinxdoc --buildsystem=pybuild override_dh_auto_build: dh_auto_build cd docs && \ PYTHONPATH=$(CURDIR) http_proxy= https_proxy= \ python3 -m sphinx -N -E -T -b html . $(CURDIR)/.pybuild/docs/html/ rm -rf $(CURDIR)/.pybuild/docs/html/.doctrees diff --git a/docs/.DS_Store b/docs/.DS_Store index 214067b..c2c4581 100644 Binary files a/docs/.DS_Store and b/docs/.DS_Store differ diff --git a/docs/index.rst b/docs/index.rst index 05c4aab..b0b288c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,288 +1,371 @@ Flask-Testing ************* .. module:: flask_testing The **Flask-Testing** extension provides unit testing utilities for Flask. Installing Flask-Testing ======================== Install with **pip** and **easy_install**:: pip install Flask-Testing or download the latest version from version control:: git clone https://github.com/jarus/flask-testing.git cd flask-testing python setup.py develop If you are using **virtualenv**, it is assumed that you are installing **Flask-Testing** in the same virtualenv as your Flask application(s). Writing tests ============= Simply subclass the ``TestCase`` class:: - from flask.ext.testing import TestCase + from flask_testing import TestCase class MyTest(TestCase): pass You must specify the ``create_app`` method, which should return a Flask instance:: from flask import Flask - from flask.ext.testing import TestCase + from flask_testing import TestCase class MyTest(TestCase): def create_app(self): app = Flask(__name__) app.config['TESTING'] = True return app If you don't define ``create_app`` a ``NotImplementedError`` will be raised. Testing with LiveServer ----------------------- If you want your tests done via Selenium or other headless browser like PhantomJS you can use the LiveServerTestCase:: import urllib2 from flask import Flask - from flask.ext.testing import LiveServerTestCase + from flask_testing import LiveServerTestCase class MyTest(LiveServerTestCase): def create_app(self): app = Flask(__name__) app.config['TESTING'] = True # Default port is 5000 app.config['LIVESERVER_PORT'] = 8943 + # Default timeout is 5 seconds + app.config['LIVESERVER_TIMEOUT'] = 10 return app def test_server_is_up_and_running(self): response = urllib2.urlopen(self.get_server_url()) self.assertEqual(response.code, 200) The method ``get_server_url`` will return http://localhost:8943 in this case. +Dynamic LiveServerTestCase port +------------------------------- + +By default, ``LiveServerTestCase`` will use the pre-defined port for running the live server. If +multiple tests need to run in parallel, the ``LIVESERVER_PORT`` can be set to ``0`` to have the +underlying operating system pick an open port for the server. The full address of the running +server can be accessed via the ``get_server_url`` call on the test case:: + + + import urllib2 + from flask import Flask + from flask_testing import LiveServerTestCase + + class MyTest(LiveServerTestCase): + + def create_app(self): + app = Flask(__name__) + app.config['TESTING'] = True + + # Set to 0 to have the OS pick the port. + app.config['LIVESERVER_PORT'] = 0 + + return app + + def test_server_is_up_and_running(self): + response = urllib2.urlopen(self.get_server_url()) + self.assertEqual(response.code, 200) + + Testing JSON responses ---------------------- If you are testing a view that returns a JSON response, you can test the output using a special ``json`` attribute appended to the ``Response`` object:: @app.route("/ajax/") def some_json(): return jsonify(success=True) class TestViews(TestCase): def test_some_json(self): response = self.client.get("/ajax/") self.assertEquals(response.json, dict(success=True)) Opt to not render the templates ------------------------------- When testing with mocks the template rendering can be a problem. If you don't want to render the templates in the tests you can use the ``render_templates`` attribute:: class TestNotRenderTemplates(TestCase): render_templates = False def test_assert_not_process_the_template(self): response = self.client.get("/template/") assert "" == response.data The signal will be sent anyway so that you can check if the template was rendered using the ``assert_template_used`` method:: class TestNotRenderTemplates(TestCase): render_templates = False def test_assert_mytemplate_used(self): response = self.client.get("/template/") self.assert_template_used('mytemplate.html') When the template rendering is turned off the tests will also run faster and the view logic can be tested in isolation. Using with Twill ---------------- `Twill`_ is a simple language for browsing the Web through a command line interface. .. note:: Please note that Twill only supports Python 2.x and therefore cannot be used with Python 3 or above. ``Flask-Testing`` comes with a helper class for creating functional tests using Twill:: def test_something_with_twill(self): with Twill(self.app, port=3000) as t: t.browser.go(t.url("/")) The older ``TwillTestCase`` has been deprecated. Testing with SQLAlchemy ----------------------- This covers a couple of points if you are using **Flask-Testing** with `SQLAlchemy`_. It is assumed that you are using the `Flask-SQLAlchemy`_ extension, but if not the examples should not be too difficult to adapt to your own particular setup. First, ensure you set the database URI to something other than your production database ! Second, it's usually a good idea to create and drop your tables with each test run, to ensure clean tests:: - from flask.ext.testing import TestCase + from flask_testing import TestCase from myapp import create_app, db class MyTest(TestCase): SQLALCHEMY_DATABASE_URI = "sqlite://" TESTING = True def create_app(self): # pass in test configuration return create_app(self) def setUp(self): db.create_all() def tearDown(self): db.session.remove() db.drop_all() Notice also that ``db.session.remove()`` is called at the end of each test, to ensure the SQLAlchemy session is properly removed and that a new session is started with each test run - this is a common "gotcha". Another gotcha is that Flask-SQLAlchemy **also** removes the session instance at the end of every request (as should any thread safe application using SQLAlchemy with **scoped_session**). Therefore the session is cleared along with any objects added to it every time you call ``client.get()`` or another client method. For example:: class SomeTest(MyTest): def test_something(self): user = User() db.session.add(user) db.session.commit() # this works assert user in db.session response = self.client.get("/") # this raises an AssertionError assert user in db.session You now have to re-add the "user" instance back to the session with ``db.session.add(user)``, if you are going to make any further database operations on it. Also notice that for this example the SQLite in-memory database is used : while it is faster for tests, if you have database-specific code (e.g. for MySQL or PostgreSQL) it may not be applicable. You may also want to add a set of instances for your database inside of a ``setUp()`` once your database tables have been created. If you want to work with larger sets of data, look at `Fixture`_ which includes support for SQLAlchemy. Running tests ============= with unittest ------------- -For the beginning I go on the theory that you put all your tests into one file -than you can use the :func:`unittest.main` function. This function will discover -all your test methods in your :class:`TestCase` classes. Remember, the test -methods and classes must starts with ``test`` (case-insensitive) that they will -discover. +I recommend you to put all your tests into one file so that you can use +the :func:`unittest.main` function. This function will discover all your test +methods in your :class:`TestCase` classes. Remember, the names of the test +methods and classes must start with ``test`` (case-insensitive) so that +they can be discovered. An example test file could look like this:: import unittest - import flask.ext.testing + import flask_testing # your test cases if __name__ == '__main__': unittest.main() Now you can run your tests with ``python tests.py``. with nose --------- The `nose`_ collector and test runner works also fine with Flask-Testing. Changes ======= +0.7.1 (19.12.2017) +------------------ + + * Reverts the request context changes from ``0.7.0``. This change broke + backwards compatibility so it will be moved to a major version release + instead. + +0.7.0 (18.12.2017) +------------------ + + * Changes the way request contexts are managed. Let's Flask be responsible + for the context, which fixes some subtle bugs. + +0.6.2 (26.02.2017) +------------------ + + * Add support for OS chosen port in ``LiveServerTestCase`` + * Better error messages when missing required modules + * ``assertRedirects`` now supports all valid redirect codes as specified + in the HTTP protocol + * Fixed bug that caused ``TypeError`` instead of ``AssertionError`` when + testing against used templates + * Fixed bug in ``assertRedirects`` where the location was not being + checked properly + +0.6.1 (03.09.2016) +------------------ + + * Fix issues that prevented tests from running when blinker was not installed + +0.6.0 (02.09.2016) +------------------ + + * ``LiveServerTestCase`` will now start running as soon as the server is up + * ``assertRedirects`` now respects the ``SERVER_NAME`` config value and can compare against absolute URLs + * Compatibility with Flask 0.11.1 + +0.5.0 (12.06.2016) +------------------ + + * Improvements to ``LiveServerTestCase`` + + * The test case will now block until the server is available + * Fixed an issue where no request context was available + * Fixed an issue where tests would be run twice when ``DEBUG`` was set to True + + * Add missing message arguments for assertRedirects and assertContext + * Better default failure message for assertRedirects + * Better default failure message for assertTemplateUsed + * Fix an issue that caused the ``render_templates`` option to not clean up after itself if set to False + * Update docs to use new Flask extension import specification + 0.4.2 (24.07.2014) ------------------ * Improved teardown to be more graceful. * Add ``message`` argument to ``assertStatus`` respectively all assertion methods with fixed status like ``assert404``. 0.4.1 (27.02.2014) ------------------ This release is dedicated to every contributer who made this release possible. Thank you very much. * Python 3 compatibility (without twill) * Add ``LiveServerTestCase`` * Use unittest2 backports if available in python 2.6 * Install multiprocessing for python versions earlier than 2.6 0.4 (06.07.2012) ---------------- * Use of the new introduced import way for flask extensions. Use ``import flask.ext.testing`` instead of ``import flaskext.testing``. * Replace all ``assert`` with ``self.assert*`` methods for better output with unittest. * Improved Python 2.5 support. * Use Flask's preferred JSON module. API === -.. module:: flask.ext.testing +.. module:: flask_testing .. autoclass:: TestCase :members: .. autoclass:: Twill :members: .. autoclass:: TwillTestCase :members: .. _Flask: http://flask.pocoo.org .. _Twill: http://twill.idyll.org/ .. _Fixture: http://farmdev.com/projects/fixture/index.html .. _SQLAlchemy: http://sqlalchemy.org .. _Flask-SQLAlchemy: http://packages.python.org/Flask-SQLAlchemy/ .. _nose: http://nose.readthedocs.org/en/latest/ diff --git a/flask_testing/utils.py b/flask_testing/utils.py index bfeae48..7362c40 100644 --- a/flask_testing/utils.py +++ b/flask_testing/utils.py @@ -1,373 +1,552 @@ # -*- coding: utf-8 -*- """ flask_testing.utils ~~~~~~~~~~~~~~~~~~~ Flask unittest integration. :copyright: (c) 2010 by Dan Jacob. :license: BSD, see LICENSE for more details. """ from __future__ import absolute_import, with_statement import gc +import multiprocessing +import socket import time + +try: + import socketserver +except ImportError: + # Python 2 SocketServer fallback + import SocketServer as socketserver + try: import unittest2 as unittest except ImportError: import unittest -import multiprocessing + +try: + from urllib.parse import urlparse, urljoin +except ImportError: + # Python 2 urlparse fallback + from urlparse import urlparse, urljoin from werkzeug import cached_property # Use Flask's preferred JSON module so that our runtime behavior matches. from flask import json_available, templating, template_rendered + +try: + from flask import message_flashed + + _is_message_flashed = True +except ImportError: + message_flashed = None + _is_message_flashed = False + if json_available: from flask import json # we'll use signals for template-related tests if # available in this version of Flask try: import blinker + _is_signals = True except ImportError: # pragma: no cover _is_signals = False __all__ = ["TestCase"] class ContextVariableDoesNotExist(Exception): pass class JsonResponseMixin(object): """ Mixin with testing helper methods """ + @cached_property def json(self): if not json_available: # pragma: no cover raise NotImplementedError return json.loads(self.data) def _make_test_response(response_class): class TestResponse(response_class, JsonResponseMixin): pass return TestResponse def _empty_render(template, context, app): """ Used to monkey patch the render_template flask method when the render_templates property is set to False in the TestCase """ if _is_signals: template_rendered.send(app, template=template, context=context) return "" -class TestCase(unittest.TestCase): +def _check_for_message_flashed_support(): + if not _is_signals or not _is_message_flashed: + raise RuntimeError( + "Your version of Flask doesn't support message_flashed. " + "This requires Flask 0.10+ with the blinker module installed." + ) + + +def _check_for_signals_support(): + if not _is_signals: + raise RuntimeError( + "Your version of Flask doesn't support signals. " + "This requires Flask 0.6+ with the blinker module installed." + ) + +class TestCase(unittest.TestCase): render_templates = True run_gc_after_test = False def create_app(self): """ Create your Flask app here, with any configuration you need. """ raise NotImplementedError def __call__(self, result=None): """ Does the required setup, doing it here means you don't have to call super.setUp in subclasses. """ try: self._pre_setup() super(TestCase, self).__call__(result) finally: self._post_teardown() + def debug(self): + try: + self._pre_setup() + super(TestCase, self).debug() + finally: + self._post_teardown() + def _pre_setup(self): self.app = self.create_app() self._orig_response_class = self.app.response_class self.app.response_class = _make_test_response(self.app.response_class) self.client = self.app.test_client() self._ctx = self.app.test_request_context() self._ctx.push() if not self.render_templates: # Monkey patch the original template render with a empty render self._original_template_render = templating._render templating._render = _empty_render self.templates = [] + self.flashed_messages = [] + if _is_signals: template_rendered.connect(self._add_template) + if _is_message_flashed: + message_flashed.connect(self._add_flash_message) + + def _add_flash_message(self, app, message, category): + self.flashed_messages.append((message, category)) + def _add_template(self, app, template, context): if len(self.templates) > 0: self.templates = [] self.templates.append((template, context)) def _post_teardown(self): if getattr(self, '_ctx', None) is not None: self._ctx.pop() del self._ctx if getattr(self, 'app', None) is not None: if getattr(self, '_orig_response_class', None) is not None: self.app.response_class = self._orig_response_class del self.app if hasattr(self, 'client'): del self.client if hasattr(self, 'templates'): del self.templates + if hasattr(self, 'flashed_messages'): + del self.flashed_messages + if _is_signals: template_rendered.disconnect(self._add_template) - if hasattr(self, '_true_render'): - templating._render = self._true_render + + if _is_message_flashed: + message_flashed.disconnect(self._add_flash_message) + + if hasattr(self, '_original_template_render'): + templating._render = self._original_template_render if self.run_gc_after_test: gc.collect() + def assertMessageFlashed(self, message, category='message'): + """ + Checks if a given message was flashed. + Only works if your version of Flask has message_flashed + signal support (0.10+) and blinker is installed. + + :param message: expected message + :param category: expected message category + """ + _check_for_message_flashed_support() + + for _message, _category in self.flashed_messages: + if _message == message and _category == category: + return True + + raise AssertionError("Message '%s' in category '%s' wasn't flashed" % (message, category)) + + assert_message_flashed = assertMessageFlashed + def assertTemplateUsed(self, name, tmpl_name_attribute='name'): """ Checks if a given template is used in the request. Only works if your version of Flask has signals support (0.6+) and blinker is installed. If the template engine used is not Jinja2, provide ``tmpl_name_attribute`` with a value of its `Template` class attribute name which contains the provided ``name`` value. :versionadded: 0.2 :param name: template name :param tmpl_name_attribute: template engine specific attribute name """ - if not _is_signals: - raise RuntimeError("Signals not supported") + _check_for_signals_support() + + used_templates = [] for template, context in self.templates: if getattr(template, tmpl_name_attribute) == name: return True - raise AssertionError("template %s not used" % name) + + used_templates.append(template) + + raise AssertionError("Template %s not used. Templates were used: %s" % (name, ' '.join(repr(used_templates)))) assert_template_used = assertTemplateUsed def get_context_variable(self, name): """ Returns a variable from the context passed to the template. Only works if your version of Flask has signals support (0.6+) and blinker is installed. Raises a ContextVariableDoesNotExist exception if does not exist in context. :versionadded: 0.2 :param name: name of variable """ - if not _is_signals: - raise RuntimeError("Signals not supported") + _check_for_signals_support() for template, context in self.templates: if name in context: return context[name] raise ContextVariableDoesNotExist - def assertContext(self, name, value): + def assertContext(self, name, value, message=None): """ Checks if given name exists in the template context and equals the given value. :versionadded: 0.2 :param name: name of context variable :param value: value to check against """ try: - self.assertEqual(self.get_context_variable(name), value) + self.assertEqual(self.get_context_variable(name), value, message) except ContextVariableDoesNotExist: - self.fail("Context variable does not exist: %s" % name) + self.fail(message or "Context variable does not exist: %s" % name) assert_context = assertContext - def assertRedirects(self, response, location): + def assertRedirects(self, response, location, message=None): """ Checks if response is an HTTP redirect to the given location. :param response: Flask response - :param location: relative URL (i.e. without **http://localhost**) + :param location: relative URL path to SERVER_NAME or an absolute URL """ - self.assertTrue(response.status_code in (301, 302)) - self.assertEqual(response.location, "http://localhost" + location) + parts = urlparse(location) + + if parts.netloc: + expected_location = location + else: + server_name = self.app.config.get('SERVER_NAME') or 'localhost' + expected_location = urljoin("http://%s" % server_name, location) + + valid_status_codes = (301, 302, 303, 305, 307) + valid_status_code_str = ', '.join(str(code) for code in valid_status_codes) + not_redirect = "HTTP Status %s expected but got %d" % (valid_status_code_str, response.status_code) + self.assertTrue(response.status_code in valid_status_codes, message or not_redirect) + self.assertEqual(response.location, expected_location, message) assert_redirects = assertRedirects def assertStatus(self, response, status_code, message=None): """ Helper method to check matching response status. :param response: Flask response :param status_code: response status code (e.g. 200) :param message: Message to display on test failure """ message = message or 'HTTP Status %s expected but got %s' \ % (status_code, response.status_code) self.assertEqual(response.status_code, status_code, message) assert_status = assertStatus def assert200(self, response, message=None): """ Checks if response status code is 200 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 200, message) assert_200 = assert200 def assert400(self, response, message=None): """ Checks if response status code is 400 :versionadded: 0.2.5 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 400, message) assert_400 = assert400 def assert401(self, response, message=None): """ Checks if response status code is 401 :versionadded: 0.2.1 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 401, message) assert_401 = assert401 def assert403(self, response, message=None): """ Checks if response status code is 403 :versionadded: 0.2 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 403, message) assert_403 = assert403 def assert404(self, response, message=None): """ Checks if response status code is 404 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 404, message) assert_404 = assert404 def assert405(self, response, message=None): """ Checks if response status code is 405 :versionadded: 0.2 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 405, message) assert_405 = assert405 def assert500(self, response, message=None): """ Checks if response status code is 500 :versionadded: 0.4.1 :param response: Flask response :param message: Message to display on test failure """ self.assertStatus(response, 500, message) assert_500 = assert500 # A LiveServerTestCase useful with Selenium or headless browsers # Inspired by https://docs.djangoproject.com/en/dev/topics/testing/#django.test.LiveServerTestCase class LiveServerTestCase(unittest.TestCase): - def create_app(self): """ Create your Flask app here, with any configuration you need. """ raise NotImplementedError def __call__(self, result=None): """ Does the required setup, doing it here means you don't have to call super.setUp in subclasses. """ # Get the app self.app = self.create_app() + self._configured_port = self.app.config.get('LIVESERVER_PORT', 5000) + self._port_value = multiprocessing.Value('i', self._configured_port) + + # We need to create a context in order for extensions to catch up + self._ctx = self.app.test_request_context() + self._ctx.push() + try: self._spawn_live_server() super(LiveServerTestCase, self).__call__(result) finally: + self._post_teardown() self._terminate_live_server() def get_server_url(self): """ Return the url of the test server """ - return 'http://localhost:%s' % self.port + return 'http://localhost:%s' % self._port_value.value def _spawn_live_server(self): self._process = None - self.port = self.app.config.get('LIVESERVER_PORT', 5000) - - worker = lambda app, port: app.run(port=port) + port_value = self._port_value + + def worker(app, port): + # Based on solution: http://stackoverflow.com/a/27598916 + # Monkey-patch the server_bind so we can determine the port bound by Flask. + # This handles the case where the port specified is `0`, which means that + # the OS chooses the port. This is the only known way (currently) of getting + # the port out of Flask once we call `run`. + original_socket_bind = socketserver.TCPServer.server_bind + def socket_bind_wrapper(self): + ret = original_socket_bind(self) + + # Get the port and save it into the port_value, so the parent process + # can read it. + (_, port) = self.socket.getsockname() + port_value.value = port + socketserver.TCPServer.server_bind = original_socket_bind + return ret + + socketserver.TCPServer.server_bind = socket_bind_wrapper + app.run(port=port, use_reloader=False) self._process = multiprocessing.Process( - target=worker, args=(self.app, self.port) + target=worker, args=(self.app, self._configured_port) ) self._process.start() - # we must wait the server start listening - time.sleep(1) + # We must wait for the server to start listening, but give up + # after a specified maximum timeout + timeout = self.app.config.get('LIVESERVER_TIMEOUT', 5) + start_time = time.time() + + while True: + elapsed_time = (time.time() - start_time) + if elapsed_time > timeout: + raise RuntimeError( + "Failed to start the server after %d seconds. " % timeout + ) + + if self._can_ping_server(): + break + + def _can_ping_server(self): + host, port = self._get_server_address() + if port == 0: + # Port specified by the user was 0, and the OS has not yet assigned + # the proper port. + return False + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.connect((host, port)) + except socket.error as e: + success = False + else: + success = True + finally: + sock.close() + + return success + + def _get_server_address(self): + """ + Gets the server address used to test the connection with a socket. + Respects both the LIVESERVER_PORT config value and overriding + get_server_url() + """ + parts = urlparse(self.get_server_url()) + + host = parts.hostname + port = parts.port + + if port is None: + if parts.scheme == 'http': + port = 80 + elif parts.scheme == 'https': + port = 443 + else: + raise RuntimeError( + "Unsupported server url scheme: %s" % parts.scheme + ) + + return host, port + + def _post_teardown(self): + if getattr(self, '_ctx', None) is not None: + self._ctx.pop() + del self._ctx def _terminate_live_server(self): if self._process: self._process.terminate() diff --git a/setup.py b/setup.py index 91ad2aa..4ea52f8 100644 --- a/setup.py +++ b/setup.py @@ -1,58 +1,58 @@ """ Flask-Testing ------------- Flask unittest integration. Links ````` * `documentation ` * `development version ` """ import sys from setuptools import setup tests_require = [ 'blinker' ] install_requires = [ 'Flask' ] if sys.version_info[0] < 3: - tests_require.append('twill==0.9') + tests_require.append('twill==0.9.1') if sys.version_info < (2, 6): tests_require.append('simplejson') install_requires.append('multiprocessing') setup( name='Flask-Testing', - version='0.4.2', + version='0.7.1', url='https://github.com/jarus/flask-testing', license='BSD', author='Dan Jacob', author_email='danjac354@gmail.com', description='Unit testing for Flask', long_description=__doc__, packages=['flask_testing'], test_suite="tests.suite", zip_safe=False, platforms='any', install_requires=install_requires, tests_require=tests_require, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ] ) diff --git a/tests/__init__.py b/tests/__init__.py index 3865848..a9296af 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,21 +1,28 @@ import unittest from flask_testing import is_twill_available -from .test_utils import TestSetup, TestSetupFailure, TestClientUtils, TestLiveServer, TestTeardownGraceful from .test_twill import TestTwill, TestTwillDeprecated +from .test_utils import TestSetup, TestSetupFailure, TestClientUtils, \ + TestLiveServer, TestTeardownGraceful, TestRenderTemplates, \ + TestNotRenderTemplates, TestRestoreTheRealRender, \ + TestLiveServerOSPicksPort def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestSetup)) suite.addTest(unittest.makeSuite(TestSetupFailure)) - suite.addTest(unittest.makeSuite(TestTeardownGraceful)) suite.addTest(unittest.makeSuite(TestClientUtils)) suite.addTest(unittest.makeSuite(TestLiveServer)) + suite.addTest(unittest.makeSuite(TestLiveServerOSPicksPort)) + suite.addTest(unittest.makeSuite(TestTeardownGraceful)) + suite.addTest(unittest.makeSuite(TestRenderTemplates)) + suite.addTest(unittest.makeSuite(TestNotRenderTemplates)) + suite.addTest(unittest.makeSuite(TestRestoreTheRealRender)) if is_twill_available: suite.addTest(unittest.makeSuite(TestTwill)) suite.addTest(unittest.makeSuite(TestTwillDeprecated)) else: print("!!! Skipping tests of Twill components\n") return suite diff --git a/tests/flask_app/__init__.py b/tests/flask_app/__init__.py index d270cfe..7ddd94e 100644 --- a/tests/flask_app/__init__.py +++ b/tests/flask_app/__init__.py @@ -1,41 +1,65 @@ -from flask import Flask, Response, abort, redirect, jsonify, render_template,\ - url_for +from flask import ( + Flask, + Response, + abort, + redirect, + jsonify, + render_template, + url_for, + flash, + request +) def create_app(): app = Flask(__name__) + app.config['SECRET_KEY'] = 'super secret testing key' @app.route("/") def index(): return Response("OK") @app.route("/template/") def index_with_template(): return render_template("index.html", name="test") + @app.route("/flash/") + def index_with_flash(): + flash("Flashed message") + return render_template("index.html") + + @app.route("/no_flash/") + def index_without_flash(): + return render_template("index.html") + @app.route("/oops/") def bad_url(): abort(404) @app.route("/redirect/") def redirect_to_index(): - return redirect(url_for("index")) + code = request.args.get('code') or 301 + return redirect(url_for("index"), code=code) + + @app.route("/external_redirect/") + def redirect_to_flask_docs(): + return redirect("http://flask.pocoo.org/") @app.route("/ajax/") def ajax(): return jsonify(name="test") @app.route("/forbidden/") def forbidden(): abort(403) @app.route("/unauthorized/") def unauthorized(): abort(401) @app.route("/internal_server_error/") def internal_server_error(): abort(500) return app diff --git a/tests/test_utils.py b/tests/test_utils.py index 1b81c80..991f510 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,209 +1,289 @@ try: from urllib2 import urlopen except ImportError: from urllib.request import urlopen from unittest import TestResult from flask_testing import TestCase, LiveServerTestCase from flask_testing.utils import ContextVariableDoesNotExist from .flask_app import create_app class TestSetup(TestCase): def create_app(self): return create_app() def test_setup(self): self.assertTrue(self.app is not None) self.assertTrue(self.client is not None) self.assertTrue(self._ctx is not None) class TestSetupFailure(TestCase): def _pre_setup(self): pass def test_setup_failure(self): '''Should not fail in _post_teardown if _pre_setup fails''' assert True class TestTeardownGraceful(TestCase): def create_app(self): return create_app() def test_remove_testcase_attributes(self): """ There should no exception after this test because teardown is graceful. """ del self.app del self._ctx class TestClientUtils(TestCase): def create_app(self): return create_app() def test_get_json(self): response = self.client.get("/ajax/") self.assertEqual(response.json, dict(name="test")) def test_status_failure_message(self): expected_message = 'my message' try: self.assertStatus(self.client.get('/'), 404, expected_message) except AssertionError as e: - self.assertTrue(expected_message in e.args[0] or \ - expected_message in e.message) + self.assertTrue(expected_message in str(e)) def test_default_status_failure_message(self): expected_message = 'HTTP Status 404 expected but got 200' try: self.assertStatus(self.client.get('/'), 404) except AssertionError as e: - self.assertTrue(expected_message in e.args[0] or \ - expected_message in e.message) + self.assertTrue(expected_message in str(e)) def test_assert_200(self): self.assert200(self.client.get("/")) def test_assert_404(self): self.assert404(self.client.get("/oops/")) def test_assert_403(self): self.assert403(self.client.get("/forbidden/")) def test_assert_401(self): self.assert401(self.client.get("/unauthorized/")) def test_assert_405(self): self.assert405(self.client.post("/")) def test_assert_500(self): self.assert500(self.client.get("/internal_server_error/")) def test_assert_redirects(self): response = self.client.get("/redirect/") self.assertRedirects(response, "/") + def test_assert_redirects_full_url(self): + response = self.client.get("/external_redirect/") + self.assertRedirects(response, "http://flask.pocoo.org/") + + def test_assert_redirects_failure_message(self): + response = self.client.get("/") + try: + self.assertRedirects(response, "/anything") + except AssertionError as e: + self.assertTrue("HTTP Status 301, 302, 303, 305, 307 expected but got 200" in str(e)) + + def test_assert_redirects_custom_message(self): + response = self.client.get("/") + try: + self.assertRedirects(response, "/anything", "Custom message") + except AssertionError as e: + self.assertTrue("Custom message" in str(e)) + + def test_assert_redirects_valid_status_codes(self): + valid_redirect_status_codes = (301, 302, 303, 305, 307) + + for status_code in valid_redirect_status_codes: + response = self.client.get("/redirect/?code=" + str(status_code)) + self.assertRedirects(response, "/") + self.assertStatus(response, status_code) + + def test_assert_redirects_invalid_status_code(self): + status_code = 200 + response = self.client.get("/redirect/?code=" + str(status_code)) + self.assertStatus(response, status_code) + try: + self.assertRedirects(response, "/") + except AssertionError as e: + self.assertTrue("HTTP Status 301, 302, 303, 305, 307 expected but got 200" in str(e)) + def test_assert_template_used(self): try: self.client.get("/template/") self.assert_template_used("index.html") except RuntimeError: pass def test_assert_template_not_used(self): - self.client.get("/") + self.client.get("/template/") try: - self.assert_template_used("index.html") - assert False - except AssertionError: - pass + self.assertRaises(AssertionError, self.assert_template_used, "invalid.html") except RuntimeError: pass def test_get_context_variable(self): try: self.client.get("/template/") self.assertEqual(self.get_context_variable("name"), "test") except RuntimeError: pass def test_assert_context(self): try: self.client.get("/template/") self.assert_context("name", "test") except RuntimeError: pass + def test_assert_context_custom_message(self): + self.client.get("/template/") + try: + self.assert_context("name", "nothing", "Custom message") + except AssertionError as e: + self.assertTrue("Custom message" in str(e)) + except RuntimeError: + pass + def test_assert_bad_context(self): try: self.client.get("/template/") self.assertRaises(AssertionError, self.assert_context, "name", "foo") self.assertRaises(AssertionError, self.assert_context, "foo", "foo") except RuntimeError: pass + def test_assert_bad_context_custom_message(self): + self.client.get("/template/") + try: + self.assert_context("foo", "foo", "Custom message") + except AssertionError as e: + self.assertTrue("Custom message" in str(e)) + except RuntimeError: + pass + def test_assert_get_context_variable_not_exists(self): try: self.client.get("/template/") self.assertRaises(ContextVariableDoesNotExist, self.get_context_variable, "foo") except RuntimeError: pass + def test_assert_flashed_messages_succeed(self): + try: + self.client.get("/flash/") + self.assertMessageFlashed("Flashed message") + except RuntimeError: + pass -class TestLiveServer(LiveServerTestCase): + def test_assert_flashed_messages_failed(self): + try: + self.client.get("/flash/") + self.assertRaises(AssertionError, self.assertMessageFlashed, "Flask-testing has assertMessageFlashed now") + except RuntimeError: + pass - def create_app(self): - app = create_app() - app.config['LIVESERVER_PORT'] = 8943 - return app + def test_assert_no_flashed_messages_fail(self): + try: + self.client.get("/no_flash/") + self.assertRaises(AssertionError, self.assertMessageFlashed, "Flashed message") + except RuntimeError: + pass + + +class BaseTestLiveServer(LiveServerTestCase): + + def test_server_process_is_spawned(self): + process = self._process + + # Check the process is spawned + self.assertNotEqual(process, None) + + # Check the process is alive + self.assertTrue(process.is_alive()) - def test_server_process_is_spawned(self): - process = self._process + def test_server_listening(self): + response = urlopen(self.get_server_url()) + self.assertTrue(b'OK' in response.read()) + self.assertEqual(response.code, 200) - # Check the process is spawned - self.assertNotEqual(process, None) - # Check the process is alive - self.assertTrue(process.is_alive()) +class TestLiveServer(BaseTestLiveServer): - def test_server_listening(self): - response = urlopen(self.get_server_url()) - self.assertTrue(b'OK' in response.read()) - self.assertEqual(response.code, 200) + def create_app(self): + app = create_app() + app.config['LIVESERVER_PORT'] = 8943 + return app + + +class TestLiveServerOSPicksPort(BaseTestLiveServer): + + def create_app(self): + app = create_app() + app.config['LIVESERVER_PORT'] = 0 + return app class TestNotRenderTemplates(TestCase): render_templates = False def create_app(self): return create_app() def test_assert_not_process_the_template(self): response = self.client.get("/template/") - assert "" == response.data + assert len(response.data) == 0 def test_assert_template_rendered_signal_sent(self): self.client.get("/template/") self.assert_template_used('index.html') class TestRenderTemplates(TestCase): render_templates = True def create_app(self): return create_app() def test_assert_not_process_the_template(self): response = self.client.get("/template/") - assert "" != response.data + assert len(response.data) > 0 class TestRestoreTheRealRender(TestCase): def create_app(self): return create_app() def test_assert_the_real_render_template_is_restored(self): test = TestNotRenderTemplates('test_assert_not_process_the_template') test_result = TestResult() test(test_result) assert test_result.wasSuccessful() response = self.client.get("/template/") - assert "" != response.data + assert len(response.data) > 0