-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add unit tests #161
Add unit tests #161
Conversation
Python projects typically do not have a `src` directory. The code is typically all located in a root pacakge under the same name as the project.
* Remove extraneous `pytest` install * Remove unused secrets * Use more detailed test runner output
With mocking, type checking becomes very difficult and annoying
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When trying to run this, i ran into an issue trying to install emoji 2.0 (per requirements.txt) but i was able to install 2.8. We might need to bunp that version in the requirements.
memebot/lib/util.py
Outdated
if TYPE_CHECKING: | ||
from discord.types.interactions import InteractionData | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just remove this if
entirely we don't need the type-check-specific import
memebot/main.py
Outdated
|
||
import config | ||
import memebot | ||
config.populate_config_from_command_line() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to go before the other imports or in main.py
at all? Could this be an import side effect by putting this in the config
module's init.py (or wherever's most appropriate)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Modified the client to be created lazily on first use, rather than at module import. Should prevent us from having to worry about side effects, similar to what was done with the logging package.
tests/commands/test_hello.py
Outdated
await commands.hello.callback(mock_interaction) | ||
|
||
mock_interaction.response.send_message.assert_awaited_once_with( | ||
f"Hello, {mock_interaction.user.display_name}!" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think this is right. The Mock here is empty, so the mock_interaction.user.display_name
will also be empty. While this test technically produces the right result, I think it would be better to populate that field so we can ensure the sent message actually accesses the property rather than something else that gives a None
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the name would have been a unique MagicMock
, but set it to a string.
tests/commands/test_poll.py
Outdated
@pytest.mark.parametrize( | ||
["a1", "a2", "a3", "a4", "a5"], | ||
# Create a matrix of all possible answer inputs that contain only 1 real answer | ||
[["Mock answer" if i == j else None for i in range(5)] for j in range(5)], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you expand this out so the test cases are more explicit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. (Thanks ChatGPT)
The tests can also be run from the _test_ Docker image: | ||
|
||
```shell | ||
$ docker run --rm -it --entrypoint python3 memebot:test -m pytest [/path/to/test/package/or/module] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't running all the tests for me:
$ docker run --rm -it --entrypoint python3 memebot:test -m pytest
================================================= test session starts ==================================================
platform linux -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0
rootdir: /opt/memebot
collected 1 item
tests/test_pytest.py . [100%]
================================================== 1 passed in 0.01s ===================================================
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by copying the tests into the test image
Instead of creating the client on load of `client.py` module, the client is instead instantiated with the first call to a new function `get_memebot()`. This eliminates side effects related to module load order.
[None, None, "Mock answer", None, None], | ||
[None, None, None, "Mock answer", None], | ||
[None, None, None, None, "Mock answer"], | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each of these should be parameters at the top level, not nested inside an array.
Also you can lose the comment because the cases are self-explanatory now that the list comp is gone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each of these should be parameters at the top level, not nested inside an array.
What is meant by this? pytest.mark.parameterize
accepts a list of parameter configurations for the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my bad, parameterize
just has a really stupid interface. Shoulda known better than to assume anything in python testing was sane...
tests/commands/test_role.py
Outdated
async def do_role_test( | ||
mock_interaction: mock.Mock, | ||
mock_guild: Optional[mock.Mock], | ||
command: discord.ext.commands.Command, | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> None: | ||
""" | ||
Helper function for common role test pattern | ||
""" | ||
mock_interaction.guild = mock_guild | ||
|
||
await command.callback(mock_interaction, *args, **kwargs) | ||
mock_interaction.response.send_message.assert_awaited_once() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be a helper. Better to explicitly do this in each case that uses it so it's more obvious what's happening in the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
tests/commands/test_role.py
Outdated
""" | ||
Test failure to create a role with an invalid name | ||
""" | ||
await do_role_create_failure( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There shouldn't be this many layers of indirection for a test. It makes it hard to understand what the test is doing. Better for test logic to be repetitive but explicit.
Same goes for the other tests in this file. Helpers would make sense for test setup but for the critical logic section of the test it should just be explicity in the test funciton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why I hate writing tests.
tests/fixtures/discord_fixtures.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are this and the twitter fixture actually used anywhere? I might just be missing something but I don't see the usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are. Fixtures are a pytest feature and they get imported and used automatically. For example, the fixture defined by
def mock_discord_client(MockClient: type[mock.Mock]) -> mock.Mock:
return MockClient()
gets called and the return value passed to every test or fixture which has a parameter with the name mock_discord_client
. It's a simple way of defining repeatable test fixtures without needing to call helper functions to use them all in every test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love implicit behavior and mocks !! Python tests and readability remain enemies
|
||
mock_interaction.response.send_message.assert_awaited_once() | ||
do_poll_embed_test(mock_interaction, question, expected_fields) | ||
await do_poll_votes_test(mock_interaction, [], yes_no=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this test be combined with the other test which uses these helpers? then the helpers can just be inlined to avoid test logic indireciton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not. The test entrypoints exist to clarify what is being tested. If we combined them, we'd just need to gate it all with if-statements anyway. I think this is fine. I'm also gonna delete the poll command and all the tests after we merge this lmao
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree but moot anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed issues. Dependency stuff can get resolved after this is merged. Could be a python version incompatibility on your end, but once tests are merged, I can enable dependabot for Python packages which should handle issues like this going forward.
[None, None, "Mock answer", None, None], | ||
[None, None, None, "Mock answer", None], | ||
[None, None, None, None, "Mock answer"], | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each of these should be parameters at the top level, not nested inside an array.
What is meant by this? pytest.mark.parameterize
accepts a list of parameter configurations for the test.
tests/fixtures/discord_fixtures.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are. Fixtures are a pytest feature and they get imported and used automatically. For example, the fixture defined by
def mock_discord_client(MockClient: type[mock.Mock]) -> mock.Mock:
return MockClient()
gets called and the return value passed to every test or fixture which has a parameter with the name mock_discord_client
. It's a simple way of defining repeatable test fixtures without needing to call helper functions to use them all in every test.
|
||
mock_interaction.response.send_message.assert_awaited_once() | ||
do_poll_embed_test(mock_interaction, question, expected_fields) | ||
await do_poll_votes_test(mock_interaction, [], yes_no=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather not. The test entrypoints exist to clarify what is being tested. If we combined them, we'd just need to gate it all with if-statements anyway. I think this is fine. I'm also gonna delete the poll command and all the tests after we merge this lmao
tests/commands/test_role.py
Outdated
async def do_role_test( | ||
mock_interaction: mock.Mock, | ||
mock_guild: Optional[mock.Mock], | ||
command: discord.ext.commands.Command, | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> None: | ||
""" | ||
Helper function for common role test pattern | ||
""" | ||
mock_interaction.guild = mock_guild | ||
|
||
await command.callback(mock_interaction, *args, **kwargs) | ||
mock_interaction.response.send_message.assert_awaited_once() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
tests/commands/test_role.py
Outdated
""" | ||
Test failure to create a role with an invalid name | ||
""" | ||
await do_role_create_failure( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why I hate writing tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks python to me
|
||
mock_interaction.response.send_message.assert_awaited_once() | ||
do_poll_embed_test(mock_interaction, question, expected_fields) | ||
await do_poll_votes_test(mock_interaction, [], yes_no=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree but moot anyway
[None, None, "Mock answer", None, None], | ||
[None, None, None, "Mock answer", None], | ||
[None, None, None, None, "Mock answer"], | ||
], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh my bad, parameterize
just has a really stupid interface. Shoulda known better than to assume anything in python testing was sane...
tests/fixtures/discord_fixtures.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love implicit behavior and mocks !! Python tests and readability remain enemies
This patch fully implements
pytest
-based unit tests for memebot. It currently tests all the commands plus Twitter integration. Most of the tests are fairly thorough.In order for the tests to run correctly, Memebot's code has been moved from the
src
directory to a rootmemebot
package, as is typical for Python projects. The docker-compose file and README have been updated to reflect this as well.Resolves #31