Skip to content

Commit fea0d16

Browse files
sijisMatthias Kay
authored andcommitted
fix: add extra_plugin_dir support to FullStackTest (errbotio#1726)
* fix: add extra_plugin_dir support to FullStackTest * docs: add example on using FullStackTest * refactor: remove deprecated extra_test_file parameter * docs: add info to CHANGES * style: format file * docs: remove travis-ci / coverall references and reordered pages (cherry picked from commit 7c587d3)
1 parent cc51138 commit fea0d16

File tree

5 files changed

+183
-38
lines changed

5 files changed

+183
-38
lines changed

CHANGES.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ fixes:
2525
- fix: type hints (#1698)
2626
- fix: update plugin config message (#1727)
2727
- docs: add example on how to use threaded replies (#1728)
28+
- fix: add extra_plugin_dir support to FullStackTest (#1726)
2829

2930

3031
v6.2.0 (2024-01-01)

docs/user_guide/plugin_development/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ with sets of recipes on a range of topics describing how to handle more advanced
2626
scheduling
2727
webhooks
2828
testing
29+
testing_plugins_fullstack
2930
logging
3031
exceptions
3132
plugin_compatibility_settings

docs/user_guide/plugin_development/testing.rst

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -251,37 +251,6 @@ You can now have a look at coverage statistics through :command:`coverage report
251251

252252
It's also possible to generate an HTML report with :command:`coverage html` and opening the resulting `htmlcov/index.html`.
253253

254-
Travis and Coveralls
255-
--------------------
256-
257-
Last but not least, you can run your tests on Travis-CI_ so when you update code or others submit pull requests the tests will automatically run confirming everything still works.
258-
259-
In order to do that you'll need a `.travis.yml` similar to this:
260-
261-
.. code-block:: yaml
262-
263-
language: python
264-
python:
265-
- 3.6
266-
- 3.7
267-
install:
268-
- pip install -q errbot pytest pytest-pep8 --use-wheel
269-
- pip install -q coverage coveralls --use-wheel
270-
script:
271-
- coverage run --source myplugin -m py.test --pep8
272-
after_success:
273-
- coveralls
274-
notifications:
275-
email: false
276-
277-
Most of it is self-explanatory, except for perhaps the `after_success`. The author of this plugin uses Coveralls.io_ to keep track of code coverage so after a successful build we call out to coveralls and upload the statistics. It's for this reason that we `pip install [..] coveralls [..]` in the `.travis.yml`.
278-
279-
The `-q` flag causes pip to be a lot more quiet and `--use-wheel` will cause pip to use wheels_ if available, speeding up your builds if you happen to depend on something that builds a C-extension.
280-
281-
Both Travis-CI and Coveralls easily integrate with Github hosted code.
282254

283255
.. _py.test: http://pytest.org
284256
.. _conftest.py: http://doc.pytest.org/en/latest/writing_plugins.html#conftest-py-local-per-directory-plugins
285-
.. _Coveralls.io: https://coveralls.io
286-
.. _Travis-CI: https://travis-ci.org
287-
.. _wheels: http://www.python.org/dev/peps/pep-0427/
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
Testing your plugins with unittest
2+
==================================
3+
4+
This guide explains how to test your Errbot plugins using the built-in testing framework. Errbot provides a powerful testing backend called ``FullStackTest`` that allows you to write unit tests for your plugins in a familiar unittest style.
5+
6+
Basic Test Setup
7+
--------------
8+
9+
To test your plugin, create a test file (e.g., `test_myplugin.py`) in your plugin's directory. Here's a basic example:
10+
11+
.. code-block:: python
12+
13+
import unittest
14+
from pathlib import Path
15+
16+
from errbot.backends.test import FullStackTest
17+
18+
path = str(Path(__file__).resolve().parent)
19+
extra_plugin_dir = path
20+
21+
22+
class TestMyPlugin(FullStackTest):
23+
def setUp(self):
24+
super().setUp(extra_plugin_dir=extra_plugin_dir)
25+
26+
def test_my_command(self):
27+
# Simulate a user sending a command
28+
self.push_message('!hello')
29+
self.assertIn('Hello!', self.pop_message())
30+
31+
Running Tests
32+
------------
33+
34+
You can run your tests using Python's unittest framework:
35+
36+
.. code-block:: bash
37+
38+
python -m unittest test_myplugin.py
39+
40+
Test Methods
41+
-----------
42+
43+
FullStackTest provides several methods to help test your plugin's behavior:
44+
45+
1. **Message Handling**:
46+
- ``push_message(command)``: Simulate a user sending a command
47+
- ``pop_message(timeout=5, block=True)``: Get the bot's response
48+
- ``assertInCommand(command, response, timeout=5)``: Assert a command returns expected output
49+
- ``assertCommandFound(command, timeout=5)``: Assert a command exists
50+
51+
2. **Room Operations**:
52+
- ``push_presence(presence)``: Simulate presence changes
53+
- Test room joining/leaving
54+
- Test room topic changes
55+
56+
3. **Plugin Management**:
57+
- ``inject_mocks(plugin_name, mock_dict)``: Inject mock objects into a plugin
58+
- Test plugin configuration
59+
- Test plugin dependencies
60+
61+
Example Test Cases
62+
----------------
63+
64+
Here are some example test cases showing different testing scenarios:
65+
66+
1. **Basic Command Testing**:
67+
68+
.. code-block:: python
69+
70+
def test_basic_command(self):
71+
self.push_message('!echo test')
72+
self.assertIn('test', self.pop_message())
73+
74+
2. **Command with Arguments**:
75+
76+
.. code-block:: python
77+
78+
def test_command_with_args(self):
79+
self.push_message('!repeat test 3')
80+
response = self.pop_message()
81+
self.assertIn('testtesttest', response)
82+
83+
3. **Error Handling**:
84+
85+
.. code-block:: python
86+
87+
def test_error_handling(self):
88+
self.push_message('!nonexistent')
89+
response = self.pop_message()
90+
self.assertIn('Command not found', response)
91+
92+
4. **Mocking Dependencies**:
93+
94+
.. code-block:: python
95+
96+
def test_with_mocks(self):
97+
# Create mock objects
98+
mock_dict = {
99+
'external_api': MockExternalAPI()
100+
}
101+
self.inject_mocks('MyPlugin', mock_dict)
102+
103+
# Test plugin behavior with mocks
104+
self.push_message('!api_test')
105+
self.assertIn('Mock response', self.pop_message())
106+
107+
Best Practices
108+
-------------
109+
110+
1. **Test Isolation**: Each test should be independent and not rely on the state from other tests.
111+
112+
2. **Setup and Teardown**: Use ``setUp()`` to initialize your test environment and ``tearDown()`` to clean up.
113+
114+
3. **Timeout Handling**: Always specify appropriate timeouts for message operations to avoid hanging tests.
115+
116+
4. **Error Cases**: Include tests for error conditions and edge cases.
117+
118+
5. **Documentation**: Document your test cases to explain what they're testing and why.
119+
120+
Complete Example
121+
--------------
122+
123+
Here's a complete example of a test suite for a plugin:
124+
125+
.. code-block:: python
126+
127+
import unittest
128+
from pathlib import Path
129+
130+
from errbot.backends.test import FullStackTest
131+
132+
path = str(Path(__file__).resolve().parent)
133+
extra_plugin_dir = path
134+
135+
class TestGreetingPlugin(FullStackTest):
136+
def setUp(self):
137+
super().setUp(extra_plugin_dir=extra_plugin_dir)
138+
139+
def test_basic_greeting(self):
140+
"""Test the basic greeting command."""
141+
self.push_message('!greet Alice')
142+
self.assertIn('Hello, Alice!', self.pop_message())
143+
144+
def test_greeting_with_options(self):
145+
"""Test greeting with different options."""
146+
# Test with count
147+
self.push_message('!greet Bob --count 2')
148+
response = self.pop_message()
149+
self.assertIn('Hello, Bob!Hello, Bob!', response)
150+
151+
# Test with shout
152+
self.push_message('!greet Charlie --shout')
153+
self.assertIn('HELLO, CHARLIE!', self.pop_message())
154+
155+
def test_error_handling(self):
156+
"""Test how the plugin handles errors."""
157+
# Test missing name
158+
self.push_message('!greet')
159+
self.assertIn('Please provide a name', self.pop_message())
160+
161+
# Test invalid count
162+
self.push_message('!greet Eve --count abc')
163+
self.assertIn('must be an integer', self.pop_message())
164+
165+
166+
if __name__ == '__main__':
167+
unittest.main()

errbot/backends/test.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import importlib
22
import logging
3+
import pathlib
34
import sys
45
import textwrap
56
import unittest
6-
from os.path import abspath, sep
7+
from os.path import sep
78
from queue import Empty, Queue
89
from tempfile import mkdtemp
910
from threading import Thread
@@ -604,25 +605,31 @@ def test_about(self):
604605
self.assertIn('Err version', self.pop_message())
605606
"""
606607

608+
def __init__(
609+
self,
610+
methodName,
611+
extra_plugin_dir=None,
612+
loglevel=logging.DEBUG,
613+
extra_config=None,
614+
):
615+
self.bot_thread = None
616+
super().__init__(methodName)
617+
607618
def setUp(
608619
self,
609620
extra_plugin_dir=None,
610-
extra_test_file=None,
611621
loglevel=logging.DEBUG,
612622
extra_config=None,
613623
) -> None:
614624
"""
615625
:param extra_plugin_dir: Path to a directory from which additional
616626
plugins should be loaded.
617-
:param extra_test_file: [Deprecated but kept for backward-compatibility,
618-
use extra_plugin_dir instead]
619-
Path to an additional plugin which should be loaded.
620627
:param loglevel: Logging verbosity. Expects one of the constants
621628
defined by the logging module.
622629
:param extra_config: Piece of extra bot config in a dict.
623630
"""
624-
if extra_plugin_dir is None and extra_test_file is not None:
625-
extra_plugin_dir = sep.join(abspath(extra_test_file).split(sep)[:-2])
631+
if extra_plugin_dir is None:
632+
extra_plugin_dir = str(pathlib.Path(".").resolve().parent.parent.absolute())
626633

627634
self.setup(
628635
extra_plugin_dir=extra_plugin_dir,

0 commit comments

Comments
 (0)