diff --git a/last_commit.txt b/last_commit.txt index 080151c3be..aff1765a51 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,44 +1,48 @@ -Repository: plone.app.layout +Repository: plone.app.testing Branch: refs/heads/master -Date: 2020-02-06T11:07:48+01:00 +Date: 2020-02-05T15:51:08+01:00 Author: ale-rt (ale-rt) -Commit: https://github.com/plone/plone.app.layout/commit/a459c8011d3c178ccd1bd9968f7534455febbc81 +Commit: https://github.com/plone/plone.app.testing/commit/e8ba5ff36f90cababf725da4a7a4b3e6c1e3120c -Analytics viewlet: make webstats_js a property +Fix some test isolation issues -Analytics viewlet: make webstats_js a property, so that it does not rely on an a call to the update method to be correctly evaluated +Fix a test isolation issue that was preventing the MOCK_MAILHOST_FIXTURE +to be used in multiple testcases (Fixes #61), -Fixes #227 +Properly configure the mail sender setting the appropriate registry +records (Fixes #62), + +Adds test coverage. Files changed: -A news/227.breaking -M news/227.bugfix -M plone/app/layout/analytics/tests/analytics.txt -M plone/app/layout/analytics/view.pt -M plone/app/layout/analytics/view.py +A news/61.bugfix +A news/62.bugfix +M src/plone/app/testing/layers.py +M src/plone/app/testing/layers.rst +M src/plone/app/testing/tests.py -b'diff --git a/news/227.breaking b/news/227.breaking\nnew file mode 100644\nindex 00000000..5d662b7a\n--- /dev/null\n+++ b/news/227.breaking\n@@ -0,0 +1 @@\n+Analytics viewlet: make webstats_js a property, so that it does not rely on an a call to the update method to be correctly evaluated [ale-rt]\ndiff --git a/news/227.bugfix b/news/227.bugfix\nindex 2d5c4765..14889926 100644\n--- a/news/227.bugfix\n+++ b/news/227.bugfix\n@@ -1 +1 @@\n-Restore compatibility with old code that was inheriting from this class and overriding the __init__ method\n+Analytics viewlet: restore compatibility with old code that was inheriting from this class and overriding the __init__ method [ale-rt]\ndiff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt\nindex 01deb6cc..9a914756 100644\n--- a/plone/app/layout/analytics/tests/analytics.txt\n+++ b/plone/app/layout/analytics/tests/analytics.txt\n@@ -12,8 +12,21 @@ We need a view on the content.\n Now we can instantiate the manager.\n \n >>> manager = Footer(portal, request, view)\n+ >>> manager.update()\n+ >>> for viewlet in manager.viewlets:\n+ ... if viewlet.__name__ == "plone.analytics":\n+ ... analytics = viewlet\n+ ... break\n+\n+When no analytics (webstats_js) code is set up the viewlet will not be rendered:\n+\n+ >>> analytics.webstats_js == u""\n+ True\n+ >>> text = manager.render()\n+ >>> \'id="plone-analytics"\' in text\n+ False\n \n-Set analytics (webstats_js) code through the controlpanel\n+Set the analytics code through the controlpanel and verify it renders properly:\n \n >>> from plone.registry.interfaces import IRegistry\n >>> from zope.component import getUtility\n@@ -21,20 +34,17 @@ Set analytics (webstats_js) code through the controlpanel\n >>> registry = getUtility(IRegistry)\n >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone")\n >>> site_settings.webstats_js = u""\n- >>> manager.update()\n+ >>> analytics.webstats_js == site_settings.webstats_js\n+ True\n >>> text = manager.render()\n+ >>> \'id="plone-analytics"\' in text\n+ True\n >>> site_settings.webstats_js in text\n True\n \n Now enter some non-ascii text\n \n >>> site_settings.webstats_js = u""\n- >>> manager.update()\n >>> text = manager.render()\n >>> site_settings.webstats_js in text\n True\n-\n-Check if the div sorrounding the script is present\n-\n- >>> \'id="plone-analytics"\' in text\n- True\ndiff --git a/plone/app/layout/analytics/view.pt b/plone/app/layout/analytics/view.pt\nindex ebd13fd5..7d3f83c7 100644\n--- a/plone/app/layout/analytics/view.pt\n+++ b/plone/app/layout/analytics/view.pt\n@@ -1,8 +1,7 @@\n-\n Here goes the webstats_js\n \n-\ndiff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py\nindex d612f8db..b45559c1 100644\n--- a/plone/app/layout/analytics/view.py\n+++ b/plone/app/layout/analytics/view.py\n@@ -16,18 +16,20 @@ class AnalyticsViewlet(BrowserView):\n def __init__(self, context, request, view, manager):\n super(AnalyticsViewlet, self).__init__(context, request)\n self.__parent__ = view\n- self.context = context\n- self.request = request\n self.view = view\n self.manager = manager\n \n- def update(self):\n- """render the webstats snippet"""\n+ @property\n+ def webstats_js(self):\n registry = getUtility(IRegistry)\n site_settings = registry.forInterface(\n ISiteSchema, prefix="plone", check=False)\n try:\n- if site_settings.webstats_js:\n- self.webstats_js = site_settings.webstats_js\n+ return site_settings.webstats_js or u""\n except AttributeError:\n- self.webstats_js = u""\n+ return u""\n+\n+ def update(self):\n+ """ The viewlet manager _updateViewlets requires this method\n+ """\n+ pass\n' +b'diff --git a/news/61.bugfix b/news/61.bugfix\nnew file mode 100644\nindex 0000000..fd1253d\n--- /dev/null\n+++ b/news/61.bugfix\n@@ -0,0 +1 @@\n+Fix a test isolation issue that was preventing the MOCK_MAILHOST_FIXTURE to be used in multiple testcases [ale-rt]\ndiff --git a/news/62.bugfix b/news/62.bugfix\nnew file mode 100644\nindex 0000000..4a8304d\n--- /dev/null\n+++ b/news/62.bugfix\n@@ -0,0 +1 @@\n+Properly configure the mail sender setting the appropriate registry records (Fixes #62)\ndiff --git a/src/plone/app/testing/layers.py b/src/plone/app/testing/layers.py\nindex fcc8775..b6d8a96 100644\n--- a/src/plone/app/testing/layers.py\n+++ b/src/plone/app/testing/layers.py\n@@ -13,6 +13,7 @@\n from plone.app.testing.interfaces import TEST_USER_PASSWORD\n from plone.app.testing.interfaces import TEST_USER_ROLES\n from plone.app.testing.utils import MockMailHost\n+from plone.registry.interfaces import IRegistry\n from plone.testing import Layer\n from plone.testing import zca\n from plone.testing import zodb\n@@ -20,6 +21,7 @@\n from plone.testing import zserver\n from Products.MailHost.interfaces import IMailHost\n from zope.component import getSiteManager\n+from zope.component import getUtility\n from zope.component.hooks import setSite\n from zope.event import notify\n from zope.traversing.interfaces import BeforeTraverseEvent\n@@ -382,19 +384,21 @@ class MockMailHostLayer(Layer):\n """\n defaultBases = (PLONE_FIXTURE,)\n \n- def setUp(self):\n+ def testSetUp(self):\n with zope.zopeApp() as app:\n portal = app[PLONE_SITE_ID]\n- portal.email_from_address = \'noreply@example.com\'\n- portal.email_from_name = \'Plone Site\'\n+ registry = getUtility(IRegistry, context=portal)\n+ if not registry["plone.email_from_address"]:\n+ registry["plone.email_from_address"] = "noreply@example.com"\n+ if not registry["plone.email_from_name"]:\n+ registry["plone.email_from_name"] = u"Plone site"\n portal._original_MailHost = portal.MailHost\n portal.MailHost = mailhost = MockMailHost(\'MailHost\')\n- portal.MailHost.smtp_host = \'localhost\'\n sm = getSiteManager(context=portal)\n sm.unregisterUtility(provided=IMailHost)\n sm.registerUtility(mailhost, provided=IMailHost)\n \n- def tearDown(self):\n+ def testTearDown(self):\n with zope.zopeApp() as app:\n portal = app[PLONE_SITE_ID]\n _o_mailhost = getattr(portal, \'_original_MailHost\', None)\ndiff --git a/src/plone/app/testing/layers.rst b/src/plone/app/testing/layers.rst\nindex 49291e8..8304321 100644\n--- a/src/plone/app/testing/layers.rst\n+++ b/src/plone/app/testing/layers.rst\n@@ -379,3 +379,98 @@ When the server is torn down, the ZServer thread is stopped.\n Traceback (most recent call last):\n ...\n requests.exceptions.ConnectionError: ...\n+\n+\n+Mock MailHost\n+~~~~~~~~~~~~~\n+\n+The fixture ``MOCK_MAILHOST_FIXTURE`` layer\n+allows to replace the Zope MailHost with a dummy one.\n+\n+**Note:** This layer builds on top of ``PLONE_FIXTURE``.\n+Like ``PLONE_FIXTURE``, it should only be used as a base layer,\n+and not directly in tests.\n+See this package\'s ``README`` file for details.\n+\n+ >>> layers.MOCK_MAILHOST_FIXTURE.__bases__\n+ (,)\n+ >>> options = runner.get_options([], [])\n+ >>> setupLayers = {}\n+ >>> runner.setup_layer(options, layers.MOCK_MAILHOST_FIXTURE, setupLayers)\n+ Set up plone.testing.zca.LayerCleanup in ... seconds.\n+ Set up plone.testing.zope.Startup in ... seconds.\n+ Set up plone.app.testing.layers.PloneFixture in ... seconds.\n+ Set up plone.app.testing.layers.MockMailHostLayer in ... seconds.\n+\n+Let\'s now simulate a test.\n+Test setup sets a couple of registry records and\n+replaces the mail host with a dummy one:\n+\n+ >>> from zope.component import getUtility\n+ >>> from plone.registry.interfaces import IRegistry\n+\n+ >>> zca.LAYER_CLEANUP.testSetUp()\n+ >>> zope.STARTUP.testSetUp()\n+ >>> layers.MOCK_MAILHOST_FIXTURE.testSetUp()\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... registry = getUtility(IRegistry, context=portal)\n+\n+ >>> registry["plone.email_from_address"]\n+ \'noreply@example.com\'\n+ >>> registry["plone.email_from_name"]\n+ \'Plone site\'\n+\n+The dummy MailHost, instead of sending the emails,\n+stores them in a list of messages:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.messages\n+ []\n+\n+If we send a message, we can check it in the list:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.send(\n+ ... "Hello world!",\n+ ... mto="foo@example.com",\n+ ... mfrom="bar@example.com",\n+ ... subject="Test",\n+ ... msg_type="text/plain",\n+ ... )\n+ >>> with helpers.ploneSite() as portal:\n+ ... for message in portal.MailHost.messages:\n+ ... print(message)\n+ MIME-Version: 1.0\n+ Content-Type: text/plain\n+ Subject: Test\n+ To: foo@example.com\n+ From: bar@example.com\n+ Date: ...\n+ \n+ Hello world!\n+\n+The list can be reset:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.reset()\n+ ... portal.MailHost.messages\n+ []\n+\n+When the test is torn down the original MaiHost is restored:\n+\n+ >>> layers.MOCK_MAILHOST_FIXTURE.testTearDown()\n+ >>> zope.STARTUP.testTearDown()\n+ >>> zca.LAYER_CLEANUP.testTearDown()\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.messages\n+ Traceback (most recent call last):\n+ ...\n+ AttributeError: \'RequestContainer\' object has no attribute \'messages\'\n+\n+ >>> runner.tear_down_unneeded(options, [], setupLayers)\n+ Tear down plone.app.testing.layers.MockMailHostLayer in ... seconds.\n+ Tear down plone.app.testing.layers.PloneFixture in ... seconds.\n+ Tear down plone.testing.zope.Startup in ... seconds.\n+ Tear down plone.testing.zca.LayerCleanup in ... seconds.\ndiff --git a/src/plone/app/testing/tests.py b/src/plone/app/testing/tests.py\nindex 22763e1..53a7a10 100644\n--- a/src/plone/app/testing/tests.py\n+++ b/src/plone/app/testing/tests.py\n@@ -1,5 +1,6 @@\n # -*- coding: utf-8 -*-\n import doctest\n+import re\n import six\n import unittest\n \n@@ -12,18 +13,38 @@ def dummy(context):\n pass\n \n \n+class Py23DocChecker(doctest.OutputChecker):\n+ def check_output(self, want, got, optionflags):\n+ if six.PY2:\n+ got = re.sub("u\'(.*?)\'", "\'\\\\1\'", got)\n+ return doctest.OutputChecker.check_output(self, want, got, optionflags)\n+\n+\n def test_suite():\n suite = unittest.TestSuite()\n # seltest = doctest.DocFileSuite(\'selenium.rst\', optionflags=OPTIONFLAGS)\n # Run selenium tests on level 2, as it requires a correctly configured\n # Firefox browser\n # seltest.level = 2\n- suite.addTests([\n- doctest.DocFileSuite(\'cleanup.rst\', optionflags=OPTIONFLAGS),\n- doctest.DocFileSuite(\'layers.rst\', optionflags=OPTIONFLAGS),\n- doctest.DocFileSuite(\'helpers.rst\', optionflags=OPTIONFLAGS),\n- # seltest,\n- ])\n+ suite.addTests(\n+ [\n+ doctest.DocFileSuite(\n+ "cleanup.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ doctest.DocFileSuite(\n+ "layers.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ doctest.DocFileSuite(\n+ "helpers.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ ]\n+ )\n if six.PY2:\n suite.addTests([\n doctest.DocFileSuite(\n' -Repository: plone.app.layout +Repository: plone.app.testing Branch: refs/heads/master -Date: 2020-02-06T14:19:07+01:00 +Date: 2020-02-07T12:03:23+01:00 Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.app.layout/commit/d4934fc75b473f67507e1b3c6e7452f5bbee5d81 +Commit: https://github.com/plone/plone.app.testing/commit/d0a6bdd1cf4839aeff6c283db92eb75597b42652 -Merge pull request #229 from plone/227-analytics-viewlet +Merge pull request #64 from plone/isolation -Analytics viewlet: make webstats_js a property +Fix some test isolation issues Files changed: -A news/227.breaking -M news/227.bugfix -M plone/app/layout/analytics/tests/analytics.txt -M plone/app/layout/analytics/view.pt -M plone/app/layout/analytics/view.py +A news/61.bugfix +A news/62.bugfix +M src/plone/app/testing/layers.py +M src/plone/app/testing/layers.rst +M src/plone/app/testing/tests.py -b'diff --git a/news/227.breaking b/news/227.breaking\nnew file mode 100644\nindex 00000000..5d662b7a\n--- /dev/null\n+++ b/news/227.breaking\n@@ -0,0 +1 @@\n+Analytics viewlet: make webstats_js a property, so that it does not rely on an a call to the update method to be correctly evaluated [ale-rt]\ndiff --git a/news/227.bugfix b/news/227.bugfix\nindex 2d5c4765..14889926 100644\n--- a/news/227.bugfix\n+++ b/news/227.bugfix\n@@ -1 +1 @@\n-Restore compatibility with old code that was inheriting from this class and overriding the __init__ method\n+Analytics viewlet: restore compatibility with old code that was inheriting from this class and overriding the __init__ method [ale-rt]\ndiff --git a/plone/app/layout/analytics/tests/analytics.txt b/plone/app/layout/analytics/tests/analytics.txt\nindex 01deb6cc..9a914756 100644\n--- a/plone/app/layout/analytics/tests/analytics.txt\n+++ b/plone/app/layout/analytics/tests/analytics.txt\n@@ -12,8 +12,21 @@ We need a view on the content.\n Now we can instantiate the manager.\n \n >>> manager = Footer(portal, request, view)\n+ >>> manager.update()\n+ >>> for viewlet in manager.viewlets:\n+ ... if viewlet.__name__ == "plone.analytics":\n+ ... analytics = viewlet\n+ ... break\n+\n+When no analytics (webstats_js) code is set up the viewlet will not be rendered:\n+\n+ >>> analytics.webstats_js == u""\n+ True\n+ >>> text = manager.render()\n+ >>> \'id="plone-analytics"\' in text\n+ False\n \n-Set analytics (webstats_js) code through the controlpanel\n+Set the analytics code through the controlpanel and verify it renders properly:\n \n >>> from plone.registry.interfaces import IRegistry\n >>> from zope.component import getUtility\n@@ -21,20 +34,17 @@ Set analytics (webstats_js) code through the controlpanel\n >>> registry = getUtility(IRegistry)\n >>> site_settings = registry.forInterface(ISiteSchema, prefix="plone")\n >>> site_settings.webstats_js = u""\n- >>> manager.update()\n+ >>> analytics.webstats_js == site_settings.webstats_js\n+ True\n >>> text = manager.render()\n+ >>> \'id="plone-analytics"\' in text\n+ True\n >>> site_settings.webstats_js in text\n True\n \n Now enter some non-ascii text\n \n >>> site_settings.webstats_js = u""\n- >>> manager.update()\n >>> text = manager.render()\n >>> site_settings.webstats_js in text\n True\n-\n-Check if the div sorrounding the script is present\n-\n- >>> \'id="plone-analytics"\' in text\n- True\ndiff --git a/plone/app/layout/analytics/view.pt b/plone/app/layout/analytics/view.pt\nindex ebd13fd5..7d3f83c7 100644\n--- a/plone/app/layout/analytics/view.pt\n+++ b/plone/app/layout/analytics/view.pt\n@@ -1,8 +1,7 @@\n-\n Here goes the webstats_js\n \n-\ndiff --git a/plone/app/layout/analytics/view.py b/plone/app/layout/analytics/view.py\nindex d612f8db..b45559c1 100644\n--- a/plone/app/layout/analytics/view.py\n+++ b/plone/app/layout/analytics/view.py\n@@ -16,18 +16,20 @@ class AnalyticsViewlet(BrowserView):\n def __init__(self, context, request, view, manager):\n super(AnalyticsViewlet, self).__init__(context, request)\n self.__parent__ = view\n- self.context = context\n- self.request = request\n self.view = view\n self.manager = manager\n \n- def update(self):\n- """render the webstats snippet"""\n+ @property\n+ def webstats_js(self):\n registry = getUtility(IRegistry)\n site_settings = registry.forInterface(\n ISiteSchema, prefix="plone", check=False)\n try:\n- if site_settings.webstats_js:\n- self.webstats_js = site_settings.webstats_js\n+ return site_settings.webstats_js or u""\n except AttributeError:\n- self.webstats_js = u""\n+ return u""\n+\n+ def update(self):\n+ """ The viewlet manager _updateViewlets requires this method\n+ """\n+ pass\n' +b'diff --git a/news/61.bugfix b/news/61.bugfix\nnew file mode 100644\nindex 0000000..fd1253d\n--- /dev/null\n+++ b/news/61.bugfix\n@@ -0,0 +1 @@\n+Fix a test isolation issue that was preventing the MOCK_MAILHOST_FIXTURE to be used in multiple testcases [ale-rt]\ndiff --git a/news/62.bugfix b/news/62.bugfix\nnew file mode 100644\nindex 0000000..4a8304d\n--- /dev/null\n+++ b/news/62.bugfix\n@@ -0,0 +1 @@\n+Properly configure the mail sender setting the appropriate registry records (Fixes #62)\ndiff --git a/src/plone/app/testing/layers.py b/src/plone/app/testing/layers.py\nindex fcc8775..b6d8a96 100644\n--- a/src/plone/app/testing/layers.py\n+++ b/src/plone/app/testing/layers.py\n@@ -13,6 +13,7 @@\n from plone.app.testing.interfaces import TEST_USER_PASSWORD\n from plone.app.testing.interfaces import TEST_USER_ROLES\n from plone.app.testing.utils import MockMailHost\n+from plone.registry.interfaces import IRegistry\n from plone.testing import Layer\n from plone.testing import zca\n from plone.testing import zodb\n@@ -20,6 +21,7 @@\n from plone.testing import zserver\n from Products.MailHost.interfaces import IMailHost\n from zope.component import getSiteManager\n+from zope.component import getUtility\n from zope.component.hooks import setSite\n from zope.event import notify\n from zope.traversing.interfaces import BeforeTraverseEvent\n@@ -382,19 +384,21 @@ class MockMailHostLayer(Layer):\n """\n defaultBases = (PLONE_FIXTURE,)\n \n- def setUp(self):\n+ def testSetUp(self):\n with zope.zopeApp() as app:\n portal = app[PLONE_SITE_ID]\n- portal.email_from_address = \'noreply@example.com\'\n- portal.email_from_name = \'Plone Site\'\n+ registry = getUtility(IRegistry, context=portal)\n+ if not registry["plone.email_from_address"]:\n+ registry["plone.email_from_address"] = "noreply@example.com"\n+ if not registry["plone.email_from_name"]:\n+ registry["plone.email_from_name"] = u"Plone site"\n portal._original_MailHost = portal.MailHost\n portal.MailHost = mailhost = MockMailHost(\'MailHost\')\n- portal.MailHost.smtp_host = \'localhost\'\n sm = getSiteManager(context=portal)\n sm.unregisterUtility(provided=IMailHost)\n sm.registerUtility(mailhost, provided=IMailHost)\n \n- def tearDown(self):\n+ def testTearDown(self):\n with zope.zopeApp() as app:\n portal = app[PLONE_SITE_ID]\n _o_mailhost = getattr(portal, \'_original_MailHost\', None)\ndiff --git a/src/plone/app/testing/layers.rst b/src/plone/app/testing/layers.rst\nindex 49291e8..8304321 100644\n--- a/src/plone/app/testing/layers.rst\n+++ b/src/plone/app/testing/layers.rst\n@@ -379,3 +379,98 @@ When the server is torn down, the ZServer thread is stopped.\n Traceback (most recent call last):\n ...\n requests.exceptions.ConnectionError: ...\n+\n+\n+Mock MailHost\n+~~~~~~~~~~~~~\n+\n+The fixture ``MOCK_MAILHOST_FIXTURE`` layer\n+allows to replace the Zope MailHost with a dummy one.\n+\n+**Note:** This layer builds on top of ``PLONE_FIXTURE``.\n+Like ``PLONE_FIXTURE``, it should only be used as a base layer,\n+and not directly in tests.\n+See this package\'s ``README`` file for details.\n+\n+ >>> layers.MOCK_MAILHOST_FIXTURE.__bases__\n+ (,)\n+ >>> options = runner.get_options([], [])\n+ >>> setupLayers = {}\n+ >>> runner.setup_layer(options, layers.MOCK_MAILHOST_FIXTURE, setupLayers)\n+ Set up plone.testing.zca.LayerCleanup in ... seconds.\n+ Set up plone.testing.zope.Startup in ... seconds.\n+ Set up plone.app.testing.layers.PloneFixture in ... seconds.\n+ Set up plone.app.testing.layers.MockMailHostLayer in ... seconds.\n+\n+Let\'s now simulate a test.\n+Test setup sets a couple of registry records and\n+replaces the mail host with a dummy one:\n+\n+ >>> from zope.component import getUtility\n+ >>> from plone.registry.interfaces import IRegistry\n+\n+ >>> zca.LAYER_CLEANUP.testSetUp()\n+ >>> zope.STARTUP.testSetUp()\n+ >>> layers.MOCK_MAILHOST_FIXTURE.testSetUp()\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... registry = getUtility(IRegistry, context=portal)\n+\n+ >>> registry["plone.email_from_address"]\n+ \'noreply@example.com\'\n+ >>> registry["plone.email_from_name"]\n+ \'Plone site\'\n+\n+The dummy MailHost, instead of sending the emails,\n+stores them in a list of messages:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.messages\n+ []\n+\n+If we send a message, we can check it in the list:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.send(\n+ ... "Hello world!",\n+ ... mto="foo@example.com",\n+ ... mfrom="bar@example.com",\n+ ... subject="Test",\n+ ... msg_type="text/plain",\n+ ... )\n+ >>> with helpers.ploneSite() as portal:\n+ ... for message in portal.MailHost.messages:\n+ ... print(message)\n+ MIME-Version: 1.0\n+ Content-Type: text/plain\n+ Subject: Test\n+ To: foo@example.com\n+ From: bar@example.com\n+ Date: ...\n+ \n+ Hello world!\n+\n+The list can be reset:\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.reset()\n+ ... portal.MailHost.messages\n+ []\n+\n+When the test is torn down the original MaiHost is restored:\n+\n+ >>> layers.MOCK_MAILHOST_FIXTURE.testTearDown()\n+ >>> zope.STARTUP.testTearDown()\n+ >>> zca.LAYER_CLEANUP.testTearDown()\n+\n+ >>> with helpers.ploneSite() as portal:\n+ ... portal.MailHost.messages\n+ Traceback (most recent call last):\n+ ...\n+ AttributeError: \'RequestContainer\' object has no attribute \'messages\'\n+\n+ >>> runner.tear_down_unneeded(options, [], setupLayers)\n+ Tear down plone.app.testing.layers.MockMailHostLayer in ... seconds.\n+ Tear down plone.app.testing.layers.PloneFixture in ... seconds.\n+ Tear down plone.testing.zope.Startup in ... seconds.\n+ Tear down plone.testing.zca.LayerCleanup in ... seconds.\ndiff --git a/src/plone/app/testing/tests.py b/src/plone/app/testing/tests.py\nindex 22763e1..53a7a10 100644\n--- a/src/plone/app/testing/tests.py\n+++ b/src/plone/app/testing/tests.py\n@@ -1,5 +1,6 @@\n # -*- coding: utf-8 -*-\n import doctest\n+import re\n import six\n import unittest\n \n@@ -12,18 +13,38 @@ def dummy(context):\n pass\n \n \n+class Py23DocChecker(doctest.OutputChecker):\n+ def check_output(self, want, got, optionflags):\n+ if six.PY2:\n+ got = re.sub("u\'(.*?)\'", "\'\\\\1\'", got)\n+ return doctest.OutputChecker.check_output(self, want, got, optionflags)\n+\n+\n def test_suite():\n suite = unittest.TestSuite()\n # seltest = doctest.DocFileSuite(\'selenium.rst\', optionflags=OPTIONFLAGS)\n # Run selenium tests on level 2, as it requires a correctly configured\n # Firefox browser\n # seltest.level = 2\n- suite.addTests([\n- doctest.DocFileSuite(\'cleanup.rst\', optionflags=OPTIONFLAGS),\n- doctest.DocFileSuite(\'layers.rst\', optionflags=OPTIONFLAGS),\n- doctest.DocFileSuite(\'helpers.rst\', optionflags=OPTIONFLAGS),\n- # seltest,\n- ])\n+ suite.addTests(\n+ [\n+ doctest.DocFileSuite(\n+ "cleanup.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ doctest.DocFileSuite(\n+ "layers.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ doctest.DocFileSuite(\n+ "helpers.rst",\n+ optionflags=OPTIONFLAGS,\n+ checker=Py23DocChecker(),\n+ ),\n+ ]\n+ )\n if six.PY2:\n suite.addTests([\n doctest.DocFileSuite(\n'