Skip to content

Commit b60ed70

Browse files
committed
Add a pytest plugin.
1 parent afb6b39 commit b60ed70

File tree

10 files changed

+293
-169
lines changed

10 files changed

+293
-169
lines changed

docs/test_clients.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ without needing to build and maintain a set of test mocks to replace the real
1212
Akismet clients. Both of these clients are configured by subclassing them and
1313
setting attributes to specify the desired behavior.
1414

15-
For examples of using these test clients, see :ref:`the testing section of the
16-
usage guide <usage-testing>`.
15+
For examples of using these test clients, including a pytest plugin for easy
16+
access to test clients, see :ref:`the testing section of the usage guide <usage-testing>`.
1717

1818
.. autoclass:: TestAsyncClient
1919
.. autoclass:: TestSyncClient

docs/testing.rst

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,78 @@ subclass:
3131
:exc:`~akismet.APIKeyError`, allowing you to test your handling of
3232
that situation.
3333

34+
See :ref:`the test client documentation <test-clients>` for details.
35+
36+
37+
Using the pytest plugin
38+
~~~~~~~~~~~~~~~~~~~~~~~
39+
40+
If you're using `pytest <https://docs.pytest.org/>`_, ``akismet`` includes a
41+
pytest plugin which provides the test clients as fixtures:
42+
43+
* ``akismet_async_client``: An instance of the async test client.
44+
45+
* ``akismet_sync_client``: An instance of the sync test client.
46+
47+
* ``akismet_async_class``: The class object for the async test client.
48+
49+
* ``akismet_sync_class``: The class object for the sync test client.
50+
51+
By default, these will succeed at key verification and will mark all content
52+
as spam. To configure the behavior, you can apply the pytest mark
53+
``akismet_client``, with arguments ``comment_check_response`` (which should be
54+
a value from the :class:`~akismet.CheckResponse` enum), and/or
55+
``verify_key_response`` (which should be a :class:`bool`). For example:
56+
57+
.. tab:: Sync
58+
59+
.. code-block:: python
60+
61+
import akismet
62+
import pytest
63+
64+
@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.DISCARD)
65+
def test_akismet_discard_response(akismet_sync_client: akismet.SyncClient):
66+
# Inside this test, akismet_sync_client's comment_check() will always
67+
# return CheckResponse.DISCARD.
68+
69+
@pytest.mark.akismet_client(verify_key_response=False)
70+
def test_akismet_fails_key_verification(akismet_sync_class: type[akismet.SyncClient]):
71+
# The key verification will always fail on this class.
72+
with pytest.raises(akismet.APIKeyError):
73+
akismet_sync_class.validated_client()
74+
75+
.. tab:: Async
76+
77+
.. code-block:: python
78+
79+
import akismet
80+
import pytest
81+
82+
@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.DISCARD)
83+
async def test_akismet_discard_response(akismet_async_client: akismet.ASyncClient):
84+
# Inside this test, akismet_async_client's comment_check() will always
85+
# return CheckResponse.DISCARD.
86+
87+
@pytest.mark.akismet_client(verify_key_response=False)
88+
async def test_akismet_fails_key_verification(akismet_async_class: type[akismet.ASyncClient]):
89+
# Key verification will always fail on this class and on all instances
90+
# of it.
91+
with pytest.raises(akismet.APIKeyError):
92+
await akismet_async_class.validated_client()
93+
94+
As a general guideline, request the client class fixtures when you want to test
95+
key verification handling in your own code, or when you're using some testing
96+
pattern which will construct instances on demand from the class, and otherwise
97+
always request a client instance fixture.
98+
99+
Testing with ``unittest``
100+
~~~~~~~~~~~~~~~~~~~~~~~~~
101+
102+
If you use the Python standard library's ``unittest`` module, or another test
103+
setup derived from it (such as Django's testing tools), you can create and use
104+
test client classes directly in your tests.
105+
34106
For example:
35107

36108
.. tab:: Sync
@@ -127,6 +199,9 @@ For example:
127199
verify_key_response = False
128200
129201
202+
Testing against the live Akismet service
203+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
204+
130205
If you also want to perform live end-to-end testing of your use of Akismet, you
131206
can do so with a real Akismet API client, by passing the optional keyword
132207
argument ``is_test=1`` to the comment-check, submit-ham, and submit-spam
@@ -142,7 +217,7 @@ certain special values for use in triggering specific responses:
142217
always cause Akismet to mark the content as not spam.
143218

144219
However, it is generally discouraged to make live requests to an external
145-
service as part of a normal test suite -- for most cases you should be making
220+
service as part of a normal test suite, For most cases you should be making
146221
use of the included test clients.
147222

148223

@@ -234,14 +309,14 @@ responses without needing to contact the live Akismet web service, so setting
234309
the environment variables for your Akismet API key and site URL is not
235310
necessary to run the normal test suite.
236311

237-
However, there is a separate test file -- found at ``tests/end_to_end.py`` --
238-
which is not run as part of the usual test suite invoked by ``nox`` and which
239-
makes live requests to Akismet. Running the tests in that file *does* require
240-
setting the ``PYTHON_AKISMET_API_KEY`` and ``PYTHON_AKISMET_BLOG_URL``
241-
environment variables to valid values, after which you can run the end-to-end
242-
tests by invoking ``nox`` and asking it to run tasks with the keyword
243-
``release`` (normally this test file is only run as a final check prior to
244-
issuing a new release, hence the keyword name):
312+
However, there is a separate test file--found at ``tests/end_to_end.py``--which
313+
is not run as part of the usual test suite invoked by ``nox`` and which makes
314+
live requests to Akismet. Running the tests in that file *does* require setting
315+
the ``PYTHON_AKISMET_API_KEY`` and ``PYTHON_AKISMET_BLOG_URL`` environment
316+
variables to valid values, after which you can run the end-to-end tests by
317+
invoking ``nox`` and asking it to run tasks with the keyword ``release``
318+
(normally this test file is only run as a final check prior to issuing a new
319+
release, hence the keyword name):
245320

246321
.. tab:: macOS/Linux/other Unix
247322

docs/usage.rst

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -423,67 +423,38 @@ And then test it like so:
423423
from your_app.moderation import flag_spam_comment
424424
425425
426-
class AlwaysSpam(akismet.TestSyncClient):
427-
"""
428-
An Akismet client whose comment_check() always returns SPAM.
429-
430-
"""
431-
comment_check_response = akismet.CheckResponse.SPAM
432-
433-
434-
class NeverSpam(akismet.TestSyncClient):
435-
"""
436-
An Akismet client whose comment_check() always returns HAM.
437-
438-
"""
439-
comment_check_response = akismet.CheckResponse.HAM
440-
441-
442-
@pytest.fixture
443-
def always_spam_client():
444-
"""
445-
pytest fixture yielding an AlwaysSpam client instance.
446-
447-
"""
448-
with AlwaysSpam() as akismet_client:
449-
yield akismet_client
450-
451-
452-
@pytest.fixture
453-
def never_spam_client():
454-
"""
455-
pytest fixture yielding a NeverSpam client instance.
456-
457-
"""
458-
with NeverSpam() as akismet_client:
459-
yield akismet_client
460-
461-
462426
# The following test functions assume you have also defined pytest
463427
# fixtures to create the request and comment objects.
464-
465-
def test_flag_set_on_spam(always_spam_client, test_request, test_comment):
428+
#
429+
#
430+
# A pytest plugin provided with akismet defines fixtures for
431+
# sync and async clients, with behavior configured by the
432+
# akismet_client mark.
433+
434+
@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.SPAM)
435+
def test_flag_set_on_spam(akismet_sync_client, test_request, test_comment):
466436
"""
467437
When the comment is identified as spam, the "filtered" attribute
468438
is set to True.
469439
470440
"""
471441
comment = flag_spam_comment(
472-
always_spam_client,
442+
akismet_sync_client,
473443
test_request,
474444
test_comment
475445
)
476446
assert comment.filtered
477447
478448
479-
def test_flag_not_set_on_non_spam(never_spam_client, test_request, test_comment):
449+
@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.HAM)
450+
def test_flag_not_set_on_non_spam(akismet_sync_client, test_request, test_comment):
480451
"""
481452
When the comment is identified as non-spam, the "filtered" attribute
482453
is set to False.
483454
484455
"""
485456
comment = flag_spam_comment(
486-
never_spam_client,
457+
akismet_sync_client,
487458
test_request,
488459
test_comment
489460
)

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ keywords = ["akismet", "spam", "spam-filtering"]
3535
license = { text = "BSD-3-Clause" }
3636
readme = "README.rst"
3737
requires-python = ">=3.9"
38-
version = "24.11.1a0"
38+
version = "25.10.1a0"
3939

4040
[dependency-groups]
4141
tests = [
@@ -44,6 +44,9 @@ tests = [
4444
"pytest",
4545
]
4646

47+
[project.entry-points.pytest11]
48+
akismet = "akismet.pytest_plugin"
49+
4750
[project.urls]
4851
"Documentation" = "https://akismet.readthedocs.io"
4952
"Source Code" = "https://github.com/ubernostrum/akismet"

0 commit comments

Comments
 (0)