Refactor all test classes into evennia.utils.test_resources. Update docs.

This commit is contained in:
Griatch 2022-01-21 00:17:24 +01:00
parent 7912351e01
commit bbf45af2dd
28 changed files with 528 additions and 588 deletions

View file

@ -1,6 +1,5 @@
# Unit Testing # Unit Testing
*Unit testing* means testing components of a program in isolation from each other to make sure every *Unit testing* means testing components of a program in isolation from each other to make sure every
part works on its own before using it with others. Extensive testing helps avoid new updates causing part works on its own before using it with others. Extensive testing helps avoid new updates causing
unexpected side effects as well as alleviates general code rot (a more comprehensive wikipedia unexpected side effects as well as alleviates general code rot (a more comprehensive wikipedia
@ -31,9 +30,9 @@ how many tests were run and how long it took. If something went wrong you will g
If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an If you contribute to Evennia, this is a useful sanity check to see you haven't introduced an
unexpected bug. unexpected bug.
## Running tests with custom settings file ## Running tests for your game dir
If you have implemented your own tests for your game (see below) you can run them from your game dir If you have implemented your own tests for your game you can run them from your game dir
with with
evennia test . evennia test .
@ -41,8 +40,8 @@ with
The period (`.`) means to run all tests found in the current directory and all subdirectories. You The period (`.`) means to run all tests found in the current directory and all subdirectories. You
could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs. could also specify, say, `typeclasses` or `world` if you wanted to just run tests in those subdirs.
Those tests will all be run using the default settings. To run the tests with your own settings file An important thing to note is that those tests will all be run using the _default Evennia settings_.
you must use the `--settings` option: To run the tests with your own settings file you must use the `--settings` option:
evennia test --settings settings.py . evennia test --settings settings.py .
@ -50,108 +49,184 @@ The `--settings` option of Evennia takes a file name in the `mygame/server/conf`
normally used to swap settings files for testing and development. In combination with `test`, it normally used to swap settings files for testing and development. In combination with `test`, it
forces Evennia to use this settings file over the default one. forces Evennia to use this settings file over the default one.
You can also test specific things by giving their path
evennia test --settings settings.py .world.tests.YourTest
## Writing new tests ## Writing new tests
Evennia's test suite makes use of Django unit test system, which in turn relies on Python's Evennia's test suite makes use of Django unit test system, which in turn relies on Python's
*unittest* module. *unittest* module.
> If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
test coverage and which does not.
To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`, To make the test runner find the tests, they must be put in a module named `test*.py` (so `test.py`,
`tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good `tests.py` etc). Such a test module will be found wherever it is in the package. It can be a good
idea to look at some of Evennia's `tests.py` modules to see how they look. idea to look at some of Evennia's `tests.py` modules to see how they look.
Inside a testing file, a `unittest.TestCase` class is used to test a single aspect or component in Inside the module you need to put a class inheriting (at any distance) from `unittest.TestCase`. Each
various ways. Each test case contains one or more *test methods* - these define the actual tests to method on that class that starts with `test_` will be run separately as a unit test. There
run. You can name the test methods anything you want as long as the name starts with "`test_`". are two special, optional methods `setUp` and `tearDown` that will (if you define them) run before
Your `TestCase` class can also have a method `setUp()`. This is run before each test, setting up and _every_ test. This can be useful for setting up and deleting things.
storing whatever preparations the test methods need. Conversely, a `tearDown()` method can
optionally do cleanup after each test.
To test the results, you use special methods of the `TestCase` class. Many of those start with To actually test things, you use special `assert...` methods on the class. Most common on is
"`assert`", such as `assertEqual` or `assertTrue`. `assertEqual`, which makes sure a result is what you expect it to be.
Example of a `TestCase` class: Here's an example of the principle. Let's assume you put this in `mygame/world/tests.py`
and want to test a function in `mygame/world/myfunctions.py`
```python ```python
# in a module tests.py somewhere i your game dir
import unittest import unittest
from evennia import create_object
# the function we want to test # the function we want to test
from mypath import myfunc from .myfunctions import myfunc
class TestObj(unittest.TestCase): class TestObj(unittest.TestCase):
"This tests a function myfunc." "This tests a function myfunc."
def setUp(self):
"""done before every of the test_ * methods below"""
self.obj = create_object("mytestobject")
def tearDown(self):
"""done after every test_* method below """
self.obj.delete()
def test_return_value(self): def test_return_value(self):
"test method. Makes sure return value is as expected." """test method. Makes sure return value is as expected."""
expected_return = "This is me being nice." actual_return = myfunc(self.obj)
actual_return = myfunc() expected_return = "This is the good object 'mytestobject'."
# test # test
self.assertEqual(expected_return, actual_return) self.assertEqual(expected_return, actual_return)
def test_alternative_call(self): def test_alternative_call(self):
"test method. Calls with a keyword argument." """test method. Calls with a keyword argument."""
expected_return = "This is me being baaaad." actual_return = myfunc(self.obj, bad=True)
actual_return = myfunc(bad=True) expected_return = "This is the baaad object 'mytestobject'."
# test # test
self.assertEqual(expected_return, actual_return) self.assertEqual(expected_return, actual_return)
``` ```
You might also want to read the [documentation for the unittest To test this, run
module](https://docs.python.org/library/unittest.html).
### Using the EvenniaTest class evennia test --settings settings.py .
Evennia offers a custom TestCase, the `evennia.utils.test_resources.EvenniaTest` class. This class to run the entire test module
initiates a range of useful properties on themselves for testing Evennia systems. Examples are
`.account` and `.session` representing a mock connected Account and its Session and `.char1` and evennia test --settings setings.py .world.tests
`char2` representing Characters complete with a location in the test database. These are all useful
when testing Evennia system requiring any of the default Evennia typeclasses as inputs. See the full or a specific class:
definition of the `EvenniaTest` class in
[evennia/utils/test_resources.py](https://github.com/evennia/evennia/blob/master/evennia/utils/test_resources.py). evennia test --settings settings.py .world.tests.TestObj
You can also run a specific test:
evennia test --settings settings.py .world.tests.TestObj.test_alternative_call
You might also want to read the [Python documentation for the unittest module](https://docs.python.org/library/unittest.html).
## Using the Evennia testing classes
Evennia offers many custom testing classes that helps with testing Evennia features.
They are all found in [evennia.utils.test_resources](evennia.utils.test_resources). Note that
these classes implement the `setUp` and `tearDown` already, so if you want to add stuff in them
yourself you should remember to use e.g. `super().setUp()` in your code.
### Classes for testing your game dir
These all use whatever setting you pass to them and works well for testing code in your game dir.
- `EvenniaTest` - this sets up a full object environment for your test. All the created entities
can be accesses as properties on the class:
- `.account` - A fake [Account](evennia.accounts.accounts.DefaultAccount) named "TestAccount".
- `.account2` - Another account named "TestAccount2"
- `char1` - A [Character](evennia.objects.objects.DefaultCharacter) linked to `.account`, named `Char`.
This has 'Developer' permissions but is not a superuser.
- `.char2` - Another character linked to `account`, named `Char2`. This has base permissions (player).
- `.obj1` - A regular [Object](evennia.objects.objects.DefaultObject) named "Obj".
- `.obj2` - Another object named "Obj2".
- `.room1` - A [Room](evennia.objects.objects.DefaultRoom) named "Room". Both characters and both
objects are located inside this room. It has a description of "room_desc".
- `.room2` - Another room named "Room2". It is empty and has no set description.
- `.exit` - An exit named "out" that leads from `.room1` to `.room2`.
- `.script` - A [Script](evennia.scripts.scripts.DefaultScript) named "Script". It's an inert script
without a timing component.
- `.session` - A fake [Session](evennia.server.serversession.ServerSession) that mimics a player
connecting to the game. It is used by `.account1` and has a sessid of 1.
- `EvenniaCommandTest` - has the same environment like `EvenniaTest` but also adds a special
[.call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call) method specifically for
testing Evennia [Commands](Commands.md). It allows you to compare what the command _actually_
returns to the player with what you expect. Read the `call` api doc for more info.
- `EvenniaTestCase` - This is identical to the regular Python `TestCase` class, it's
just there for naming symmetry with `BaseEvenniaTestCase` below.
Here's an example of using `EvenniaTest`
```python ```python
# in a test module # in a test module
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import EvenniaTest
class TestObject(EvenniaTest):
class TestObject(BaseEvenniaTest): """Remember that the testing class creates char1 and char2 inside room1 ..."""
def test_object_search(self): def test_object_search_character(self):
# char1 and char2 are both created in room1 """Check that char1 can search for char2 by name"""
self.assertEqual(self.char1.search(self.char2.key), self.char2) self.assertEqual(self.char1.search(self.char2.key), self.char2)
def test_location_search(self):
"""Check so that char1 can find the current location by name"""
self.assertEqual(self.char1.search(self.char1.location.key), self.char1.location) self.assertEqual(self.char1.search(self.char1.location.key), self.char1.location)
# ... # ...
``` ```
### Testing in-game Commands This example tests a custom command.
In-game Commands are a special case. Tests for the default commands are put in
`evennia/commands/default/tests.py`. This uses a custom `CommandTest` class that inherits from
`evennia.utils.test_resources.EvenniaTest` described above. `CommandTest` supplies extra convenience
functions for executing commands and check that their return values (calls of `msg()` returns
expected values. It uses Characters and Sessions generated on the `EvenniaTest` class to call each
class).
Each command tested should have its own `TestCase` class. Inherit this class from the `CommandTest`
class in the same module to get access to the command-specific utilities mentioned.
```python ```python
from evennia.commands.default.tests import CommandTest from evennia.commands.default.tests import EvenniaCommandTest
from evennia.commands.default import general from commands import command as mycommand
class TestSet(CommandTest):
"tests the look command by simple call, using Char2 as a target"
def test_mycmd_char(self): class TestSet(EvenniaCommandTest):
self.call(general.CmdLook(), "Char2", "Char2(#7)") "tests the look command by simple call, using Char2 as a target"
def test_mycmd_char(self):
self.call(mycommand.CmdMyLook(), "Char2", "Char2(#7)")
def test_mycmd_room(self):
"tests the look command by simple call, with target as room" "tests the look command by simple call, with target as room"
def test_mycmd_room(self): self.call(mycommand.CmdMyLook(), "Room",
self.call(general.CmdLook(), "Room", "Room(#1)\nroom_desc\nExits: out(#3)\n"
"Room(#1)\nroom_desc\nExits: out(#3)\n" "You see: Obj(#4), Obj2(#5), Char2(#7)")
"You see: Obj(#4), Obj2(#5), Char2(#7)")
``` ```
### Unit testing contribs with custom models When using `.call`, you don't need to specify the entire string; you can just give the beginning
of it and if it matches, that's enough. Use `\n` to denote line breaks and (this is a special for
the `.call` helper), `||` to indicate multiple uses of `.msg()` in the Command. The `.call` helper
has a lot of arguments for mimicing different ways of calling a Command, so make sure to
[read the API docs for .call()](evennia.utils.test_resources.EvenniaCommandTestMixin.call).
### Classes for testing Evennia core
These are used for testing Evennia itself. They provide the same resources as the classes
above but enforce Evennias default settings found in `evennia/settings_default.py`, ignoring
any settings changes in your game dir.
- `BaseEvenniaTest` - all the default objects above but with enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, but with enforced default settings
- `BaseEvenniaTestCase` - no default objects, only enforced default settings
There are also two special 'mixin' classes. These are uses in the classes above, but may also
be useful if you want to mix your own testing classes:
- `EvenniaTestMixin` - A class mixin that creates all test environment objects.
- `EvenniaCommandMixin` - A class mixin that adds the `.call()` Command-tester helper.
If you want to help out writing unittests for Evennia, take a look at Evennia's [coveralls.io
page](https://coveralls.io/github/evennia/evennia). There you see which modules have any form of
test coverage and which does not. All help is appreciated!
## Unit testing contribs with custom models
A special case is if you were to create a contribution to go to the `evennia/contrib` folder that A special case is if you were to create a contribution to go to the `evennia/contrib` folder that
uses its [own database models](../Concepts/New-Models.md). The problem with this is that Evennia (and Django) will uses its [own database models](../Concepts/New-Models.md). The problem with this is that Evennia (and Django) will
@ -216,14 +291,8 @@ class TestMyModel(BaseEvenniaTest):
# test case here # test case here
``` ```
### A note on adding new tests
Having an extensive tests suite is very important for avoiding code degradation as Evennia is ## A note on making the test runner faster
developed. Only a small fraction of the Evennia codebase is covered by test suites at this point.
Writing new tests is not hard, it's more a matter of finding the time to do so. So adding new tests
is really an area where everyone can contribute, also with only limited Python skills.
### A note on making the test runner faster
If you have custom models with a large number of migrations, creating the test database can take a If you have custom models with a large number of migrations, creating the test database can take a
very long time. If you don't require migrations to run for your tests, you can disable them with the very long time. If you don't require migrations to run for your tests, you can disable them with the
@ -247,155 +316,3 @@ After doing so, you can then run tests without migrations by adding the `--nomig
``` ```
evennia test --settings settings.py --nomigrations . evennia test --settings settings.py --nomigrations .
``` ```
## Testing for Game development (mini-tutorial)
Unit testing can be of paramount importance to game developers. When starting with a new game, it is
recommended to look into unit testing as soon as possible; an already huge game is much harder to
write tests for. The benefits of testing a game aren't different from the ones regarding library
testing. For example it is easy to introduce bugs that affect previously working code. Testing is
there to ensure your project behaves the way it should and continue to do so.
If you have never used unit testing (with Python or another language), you might want to check the
[official Python documentation about unit testing](https://docs.python.org/2/library/unittest.html),
particularly the first section dedicated to a basic example.
### Basic testing using Evennia
Evennia's test runner can be used to launch tests in your game directory (let's call it 'mygame').
Evennia's test runner does a few useful things beyond the normal Python unittest module:
* It creates and sets up an empty database, with some useful objects (accounts, characters and
rooms, among others).
* It provides simple ways to test commands, which can be somewhat tricky at times, if not tested
properly.
Therefore, you should use the command-line to execute the test runner, while specifying your own
game directories (not the one containing evennia). Go to your game directory (referred as 'mygame'
in this section) and execute the test runner:
evennia --settings settings.py test commands
This command will execute Evennia's test runner using your own settings file. It will set up a dummy
database of your choice and look into the 'commands' package defined in your game directory
(`mygame/commands` in this example) to find tests. The test module's name should begin with 'test'
and contain one or more `TestCase`. A full example can be found below.
### A simple example
In your game directory, go to `commands` and create a new file `tests.py` inside (it could be named
anything starting with `test`). We will start by making a test that has nothing to do with Commands,
just to show how unit testing works:
```python
# mygame/commands/tests.py
import unittest
class TestString(unittest.TestCase):
"""Unittest for strings (just a basic example)."""
def test_upper(self):
"""Test the upper() str method."""
self.assertEqual('foo'.upper(), 'FOO')
```
This example, inspired from the Python documentation, is used to test the 'upper()' method of the
'str' class. Not very useful, but it should give you a basic idea of how tests are used.
Let's execute that test to see if it works.
> evennia --settings settings.py test commands
TESTING: Using specified settings file 'server.conf.settings'.
(Obs: Evennia's full test suite may not pass if the settings are very
different from the default. Use 'test .' as arguments to run only tests
on the game dir.)
Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Destroying test database for alias 'default'...
We specified the `commands` package to the evennia test command since that's where we put our test
file. In this case we could just as well just said `.` to search all of `mygame` for testing files.
If we have a lot of tests it may be useful to test only a single set at a time though. We get an
information text telling us we are using our custom settings file (instead of Evennia's default
file) and then the test runs. The test passes! Change the "FOO" string to something else in the test
to see how it looks when it fails.
### Testing commands
```{warning} This is not correct anymore.
```
This section will test the proper execution of the 'abilities' command, as described in the DELETED
tutorial to create the 'abilities' command, we will need it to test it.
Testing commands in Evennia is a bit more complex than the simple testing example we have seen.
Luckily, Evennia supplies a special test class to do just that ... we just need to inherit from it
and use it properly. This class is called 'CommandTest' and is defined in the
'evennia.commands.default.tests' package. To create a test for our 'abilities' command, we just
need to create a class that inherits from 'CommandTest' and add methods.
We could create a new test file for this but for now we just append to the `tests.py` file we
already have in `commands` from before.
```python
# bottom of mygame/commands/tests.py
from evennia.commands.default.tests import CommandTest
from commands.command import CmdAbilities
from typeclasses.characters import Character
class TestAbilities(CommandTest):
character_typeclass = Character
def test_simple(self):
self.call(CmdAbilities(), "", "STR: 5, AGI: 4, MAG: 2")
```
* Line 1-4: we do some importing. 'CommandTest' is going to be our base class for our test, so we
need it. We also import our command ('CmdAbilities' in this case). Finally we import the
'Character' typeclass. We need it, since 'CommandTest' doesn't use 'Character', but
'DefaultCharacter', which means the character calling the command won't have the abilities we have
written in the 'Character' typeclass.
* Line 6-8: that's the body of our test. Here, a single command is tested in an entire class.
Default commands are usually grouped by category in a single class. There is no rule, as long as
you know where you put your tests. Note that we set the 'character_typeclass' class attribute to
Character. As explained above, if you didn't do that, the system would create a 'DefaultCharacter'
object, not a 'Character'. You can try to remove line 4 and 8 to see what happens when running the
test.
* Line 10-11: our unique testing method. Note its name: it should begin by 'test_'. Apart from
that, the method is quite simple: it's an instance method (so it takes the 'self' argument) but no
other arguments are needed. Line 11 uses the 'call' method, which is defined in 'CommandTest'.
It's a useful method that compares a command against an expected result. It would be like comparing
two strings with 'assertEqual', but the 'call' method does more things, including testing the
command in a realistic way (calling its hooks in the right order, so you don't have to worry about
that).
Line 11 can be understood as: test the 'abilities' command (first parameter), with no argument
(second parameter), and check that the character using it receives his/her abilities (third
parameter).
Let's run our new test:
> evennia --settings settings.py test commands
[...]
Creating test database for alias 'default'...
..
----------------------------------------------------------------------
Ran 2 tests in 0.156s
OK
Destroying test database for alias 'default'...
Two tests were executed, since we have kept 'TestString' from last time. In case of failure, you
will get much more information to help you fix the bug.

View file

@ -11,8 +11,6 @@ main test suite started with
> python game/manage.py test. > python game/manage.py test.
""" """
import re
import types
import datetime import datetime
from anything import Anything from anything import Anything
@ -23,7 +21,7 @@ from unittest.mock import patch, Mock, MagicMock
from evennia import DefaultRoom, DefaultExit, ObjectDB from evennia import DefaultRoom, DefaultExit, ObjectDB
from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaTest from evennia.utils.test_resources import BaseEvenniaTest, BaseEvenniaCommandTest, EvenniaCommandTest # noqa
from evennia.commands.default import ( from evennia.commands.default import (
help as help_module, help as help_module,
general, general,
@ -40,305 +38,18 @@ from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi, utils, gametime, create from evennia.utils import utils, gametime, create
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia import search_object from evennia import search_object
from evennia import DefaultObject, DefaultCharacter from evennia import DefaultObject, DefaultCharacter
from evennia.prototypes import prototypes as protlib from evennia.prototypes import prototypes as protlib
# set up signal here since we are not starting the server
_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# ------------------------------------------------------------ # ------------------------------------------------------------
# Command testing # Command testing
# ------------------------------------------------------------ # ------------------------------------------------------------
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class CommandTestMixin:
"""
Mixin to add to a test in order to provide the `.call` helper for
testing the execution and returns of a command.
Tests a Command by running it and comparing what messages it sends with class TestGeneral(BaseEvenniaCommandTest):
expected values. This tests without actually spinning up the cmdhandler
for every test, which is more controlled.
Example:
::
from commands.echo import CmdEcho
class MyCommandTest(EvenniaTest, CommandTestMixin):
def test_echo(self):
'''
Test that the echo command really returns
what you pass into it.
'''
self.call(MyCommand(), "hello world!",
"You hear your echo: 'Hello world!'")
"""
# formatting for .call's error message
_ERROR_FORMAT = """
=========================== Wanted message ===================================
{expected_msg}
=========================== Returned message =================================
{returned_msg}
==============================================================================
""".rstrip()
def call(
self,
cmdobj,
input_args,
msg=None,
cmdset=None,
noansi=True,
caller=None,
receiver=None,
cmdstring=None,
obj=None,
inputs=None,
raw_string=None,
):
"""
Test a command by assigning all the needed properties to a cmdobj and
running the sequence. The resulting `.msg` calls will be mocked and
the text= calls to them compared to a expected output.
Args:
cmdobj (Command): The command object to use.
input_args (str): This should be the full input the Command should
see, such as 'look here'. This will become `.args` for the Command
instance to parse.
msg (str or dict, optional): This is the expected return value(s)
returned through `caller.msg(text=...)` calls in the command. If a string, the
receiver is controlled with the `receiver` kwarg (defaults to `caller`).
If this is a `dict`, it is a mapping
`{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
ignored. The message(s) are compared with the actual messages returned
to the receiver(s) as the Command runs. Each check uses `.startswith`,
so you can choose to only include the first part of the
returned message if that's enough to verify a correct result. EvMenu
decorations (like borders) are stripped and should not be included. This
should also not include color tags unless `noansi=False`.
If the command returns texts in multiple separate `.msg`-
calls to a receiver, separate these with `|` if `noansi=True`
(default) and `||` if `noansi=False`. If no `msg` is given (`None`),
then no automatic comparison will be done.
cmdset (str, optional): If given, make `.cmdset` available on the Command
instance as it runs. While `.cmdset` is normally available on the
Command instance by default, this is usually only used by
commands that explicitly operates/displays cmdsets, like
`examine`.
noansi (str, optional): By default the color tags of the `msg` is
ignored, this makes them significant. If unset, `msg` must contain
the same color tags as the actual return message.
caller (Object or Account, optional): By default `self.char1` is used as the
command-caller (the `.caller` property on the Command). This allows to
execute with another caller, most commonly an Account.
receiver (Object or Account, optional): This is the object to receive the
return messages we want to test. By default this is the same as `caller`
(which in turn defaults to is `self.char1`). Note that if `msg` is
a `dict`, this is ignored since the receiver is already specified there.
cmdstring (str, optional): Normally this is the Command's `key`.
This allows for tweaking the `.cmdname` property of the
Command`. This isb used for commands with multiple aliases,
where the command explicitly checs which alias was used to
determine its functionality.
obj (str, optional): This sets the `.obj` property of the Command - the
object on which the Command 'sits'. By default this is the same as `caller`.
This can be used for testing on-object Command interactions.
inputs (list, optional): A list of strings to pass to functions that pause to
take input from the user (normally using `@interactive` and
`ret = yield(question)` or `evmenu.get_input`). Each element of the
list will be passed into the command as if the user wrote that at the prompt.
raw_string (str, optional): Normally the `.raw_string` property is set as
a combination of your `key/cmdname` and `input_args`. This allows
direct control of what this is, for example for testing edge cases
or malformed inputs.
Returns:
str or dict: The message sent to `receiver`, or a dict of
`{receiver: "msg", ...}` if multiple are given. This is usually
only used with `msg=None` to do the validation externally.
Raises:
AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
match `expected_input`.
Notes:
As part of the tests, all methods of the Command will be called in
the proper order:
- cmdobj.at_pre_cmd()
- cmdobj.parse()
- cmdobj.func()
- cmdobj.at_post_cmd()
"""
# The `self.char1` is created in the `EvenniaTest` base along with
# other helper objects like self.room and self.obj
caller = caller if caller else self.char1
cmdobj.caller = caller
cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
cmdobj.raw_cmdname = cmdobj.cmdname
cmdobj.cmdstring = cmdobj.cmdname # deprecated
cmdobj.args = input_args
cmdobj.cmdset = cmdset
cmdobj.session = SESSIONS.session_from_sessid(1)
cmdobj.account = self.account
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + input_args
cmdobj.obj = obj or (caller if caller else self.char1)
inputs = inputs or []
# set up receivers
receiver_mapping = {}
if isinstance(msg, dict):
# a mapping {receiver: msg, ...}
receiver_mapping = {recv: str(msg).strip() if msg else None
for recv, msg in msg.items()}
else:
# a single expected string and thus a single receiver (defaults to caller)
receiver = receiver if receiver else caller
receiver_mapping[receiver] = str(msg).strip() if msg is not None else None
unmocked_msg_methods = {}
for receiver in receiver_mapping:
# save the old .msg method so we can get it back
# cleanly after the test
unmocked_msg_methods[receiver] = receiver.msg
# replace normal `.msg` with a mock
receiver.msg = Mock()
# Run the methods of the Command. This mimics what happens in the
# cmdhandler. This will have the mocked .msg be called as part of the
# execution. Mocks remembers what was sent to them so we will be able
# to retrieve what was sent later.
try:
if cmdobj.at_pre_cmd():
return
cmdobj.parse()
ret = cmdobj.func()
# handle func's with yield in them (making them generators)
if isinstance(ret, types.GeneratorType):
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
# this mimics a user's reply to a prompt
ret.send(inp)
except TypeError:
next(ret)
ret = ret.send(inp)
else:
# non-input yield, like yield(10). We don't pause
# but fire it immediately.
next(ret)
except StopIteration:
break
cmdobj.at_post_cmd()
except StopIteration:
pass
except InterruptCommand:
pass
for inp in inputs:
# if there are any inputs left, we may have a non-generator
# input to handle (get_input/ask_yes_no that uses a separate
# cmdset rather than a yield
caller.execute_cmd(inp)
# At this point the mocked .msg methods on each receiver will have
# stored all calls made to them (that's a basic function of the Mock
# class). We will not extract them and compare to what we expected to
# go to each receiver.
returned_msgs = {}
for receiver, expected_msg in receiver_mapping.items():
# get the stored messages from the Mock with Mock.mock_calls.
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs))
for name, args, kwargs in receiver.msg.mock_calls
]
# we can return this now, we are done using the mock
receiver.msg = unmocked_msg_methods[receiver]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [str(smsg[0])
if isinstance(smsg, tuple) else str(smsg) for smsg in stored_msg]
if expected_msg is None:
# no expected_msg; just build the returned_msgs dict
returned_msg = "\n".join(str(msg) for msg in stored_msg)
returned_msgs[receiver] = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
else:
# compare messages to expected
# set our separator for returned messages based on parsing ansi or not
msg_sep = "|" if noansi else "||"
# We remove Evmenu decorations since that just makes it harder
# to write the comparison string. We also strip ansi before this
# comparison since otherwise it would mess with the regex.
returned_msg = msg_sep.join(
_RE_STRIP_EVMENU.sub(
"", ansi.parse_ansi(mess, strip_ansi=noansi))
for mess in stored_msg).strip()
# this is the actual test
if expected_msg == "" and returned_msg or not returned_msg.startswith(expected_msg):
# failed the test
raise AssertionError(
self._ERROR_FORMAT.format(
expected_msg=expected_msg, returned_msg=returned_msg)
)
# passed!
returned_msgs[receiver] = returned_msg
if len(returned_msgs) == 1:
return list(returned_msgs.values())[0]
return returned_msgs
@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS", MuxCommand)
class EvenniaCommandTest(BaseEvenniaTest, CommandTestMixin):
"""
Commands only using the default settings.
"""
class CommandTest(EvenniaTest, CommandTestMixin):
"""
Parent class to inherit from - makes tests use your own
classes and settings in mygame.
"""
# ------------------------------------------------------------
# Individual module Tests
# ------------------------------------------------------------
class TestGeneral(EvenniaCommandTest):
def test_look(self): def test_look(self):
rid = self.room1.id rid = self.room1.id
self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid)) self.call(general.CmdLook(), "here", "Room(#{})\nroom_desc".format(rid))
@ -434,7 +145,7 @@ class TestGeneral(EvenniaCommandTest):
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):") self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
class TestHelp(EvenniaCommandTest): class TestHelp(BaseEvenniaCommandTest):
maxDiff = None maxDiff = None
@ -584,7 +295,7 @@ class TestHelp(EvenniaCommandTest):
cmdset=TestCmdSet()) cmdset=TestCmdSet())
class TestSystem(EvenniaCommandTest): class TestSystem(BaseEvenniaCommandTest):
def test_py(self): def test_py(self):
# we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime # we are not testing CmdReload, CmdReset and CmdShutdown, CmdService or CmdTime
# since the server is not running during these tests. # since the server is not running during these tests.
@ -608,7 +319,7 @@ _TASK_HANDLER = None
def func_test_cmd_tasks(): def func_test_cmd_tasks():
return 'success' return 'success'
class TestCmdTasks(EvenniaCommandTest): class TestCmdTasks(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -768,7 +479,7 @@ class TestCmdTasks(EvenniaCommandTest):
self.call(system.CmdTasks(), f'/cancel', wanted_msg) self.call(system.CmdTasks(), f'/cancel', wanted_msg)
class TestAdmin(EvenniaCommandTest): class TestAdmin(BaseEvenniaCommandTest):
def test_emit(self): def test_emit(self):
self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest") self.call(admin.CmdEmit(), "Char2 = Test", "Emitted to Char2:\nTest")
@ -799,7 +510,7 @@ class TestAdmin(EvenniaCommandTest):
) )
class TestAccount(EvenniaCommandTest): class TestAccount(BaseEvenniaCommandTest):
def test_ooc_look(self): def test_ooc_look(self):
if settings.MULTISESSION_MODE < 2: if settings.MULTISESSION_MODE < 2:
self.call( self.call(
@ -923,7 +634,7 @@ class TestAccount(EvenniaCommandTest):
) )
class TestBuilding(EvenniaCommandTest): class TestBuilding(BaseEvenniaCommandTest):
def test_create(self): def test_create(self):
name = settings.BASE_OBJECT_TYPECLASS.rsplit(".", 1)[1] name = settings.BASE_OBJECT_TYPECLASS.rsplit(".", 1)[1]
self.call( self.call(
@ -1991,7 +1702,7 @@ from evennia.comms.comms import DefaultChannel # noqa
@patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel) @patch("evennia.commands.default.comms.CHANNEL_DEFAULT_TYPECLASS", DefaultChannel)
class TestCommsChannel(EvenniaCommandTest): class TestCommsChannel(BaseEvenniaCommandTest):
""" """
Test the central `channel` command. Test the central `channel` command.
@ -2214,7 +1925,7 @@ class TestCommsChannel(EvenniaCommandTest):
from evennia.commands.default import comms # noqa from evennia.commands.default import comms # noqa
class TestComms(EvenniaCommandTest): class TestComms(BaseEvenniaCommandTest):
def test_page(self): def test_page(self):
self.call( self.call(
@ -2226,7 +1937,7 @@ class TestComms(EvenniaCommandTest):
) )
class TestBatchProcess(EvenniaCommandTest): class TestBatchProcess(BaseEvenniaCommandTest):
""" """
Test the batch processor. Test the batch processor.
@ -2262,13 +1973,13 @@ class CmdInterrupt(Command):
self.msg("in func") self.msg("in func")
class TestInterruptCommand(EvenniaCommandTest): class TestInterruptCommand(BaseEvenniaCommandTest):
def test_interrupt_command(self): def test_interrupt_command(self):
ret = self.call(CmdInterrupt(), "") ret = self.call(CmdInterrupt(), "")
self.assertEqual(ret, "") self.assertEqual(ret, "")
class TestUnconnectedCommand(EvenniaCommandTest): class TestUnconnectedCommand(BaseEvenniaCommandTest):
def test_info_command(self): def test_info_command(self):
# instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower # instead of using SERVER_START_TIME (0), we use 86400 because Windows won't let us use anything lower
gametime.SERVER_START_TIME = 86400 gametime.SERVER_START_TIME = 86400
@ -2288,7 +1999,7 @@ class TestUnconnectedCommand(EvenniaCommandTest):
# Test syscommands # Test syscommands
class TestSystemCommands(EvenniaCommandTest): class TestSystemCommands(BaseEvenniaCommandTest):
def test_simple_defaults(self): def test_simple_defaults(self):
self.call(syscommands.SystemNoInput(), "") self.call(syscommands.SystemNoInput(), "")
self.call(syscommands.SystemNoMatch(), "Huh?") self.call(syscommands.SystemNoMatch(), "Huh?")

View file

@ -3,7 +3,7 @@ Building menu tests.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . building_menu import BuildingMenu, CmdNoMatch from . building_menu import BuildingMenu, CmdNoMatch
@ -12,7 +12,7 @@ class Submenu(BuildingMenu):
self.add_choice("title", key="t", attr="key") self.add_choice("title", key="t", attr="key")
class TestBuildingMenu(EvenniaCommandTest): class TestBuildingMenu(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super(TestBuildingMenu, self).setUp() super(TestBuildingMenu, self).setUp()
self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test") self.menu = BuildingMenu(caller=self.char1, obj=self.room1, title="test")

View file

@ -3,11 +3,11 @@ Test email login.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import email_login from . import email_login
class TestEmailLogin(EvenniaCommandTest): class TestEmailLogin(BaseEvenniaCommandTest):
def test_connect(self): def test_connect(self):
self.call( self.call(
email_login.CmdUnconnectedConnect(), email_login.CmdUnconnectedConnect(),

View file

@ -7,7 +7,7 @@ from textwrap import dedent
from django.conf import settings from django.conf import settings
from evennia import ScriptDB from evennia import ScriptDB
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.objects.objects import ExitCommand from evennia.objects.objects import ExitCommand
from evennia.utils import ansi, utils from evennia.utils import ansi, utils
from evennia.utils.create import create_object, create_script from evennia.utils.create import create_object, create_script
@ -246,7 +246,7 @@ class TestEventHandler(BaseEvenniaTest):
self.assertEqual(self.room1.callbacks.all(), {}) self.assertEqual(self.room1.callbacks.all(), {})
class TestCmdCallback(EvenniaCommandTest): class TestCmdCallback(BaseEvenniaCommandTest):
"""Test the @callback command.""" """Test the @callback command."""
@ -425,7 +425,7 @@ class TestCmdCallback(EvenniaCommandTest):
self.assertEqual(callback.valid, True) self.assertEqual(callback.valid, True)
class TestDefaultCallbacks(EvenniaCommandTest): class TestDefaultCallbacks(BaseEvenniaCommandTest):
"""Test the default callbacks.""" """Test the default callbacks."""

View file

@ -3,10 +3,10 @@ Test menu_login
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import menu_login from . import menu_login
class TestMenuLogin(EvenniaCommandTest): class TestMenuLogin(BaseEvenniaCommandTest):
def test_cmdunloggedlook(self): def test_cmdunloggedlook(self):
self.call(menu_login.CmdUnloggedinLook(), "", "======") self.call(menu_login.CmdUnloggedinLook(), "", "======")

View file

@ -3,11 +3,11 @@ Legacy Mux comms tests (extracted from 0.9.5)
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mux_comms_cmds as comms from . import mux_comms_cmds as comms
class TestLegacyMuxComms(EvenniaCommandTest): class TestLegacyMuxComms(BaseEvenniaCommandTest):
""" """
Test the legacy comms contrib. Test the legacy comms contrib.
""" """

View file

@ -3,7 +3,7 @@ Test of the Unixcommand.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from .unixcommand import UnixCommand from .unixcommand import UnixCommand
@ -30,7 +30,7 @@ class CmdDummy(UnixCommand):
self.msg("{} * {} = {}".format(nb1, nb2, result)) self.msg("{} * {} = {}".format(nb1, nb2, result))
class TestUnixCommand(EvenniaCommandTest): class TestUnixCommand(BaseEvenniaCommandTest):
def test_success(self): def test_success(self):
"""See the command parsing succeed.""" """See the command parsing succeed."""
self.call(CmdDummy(), "5 10", "5 * 10 = 50") self.call(CmdDummy(), "5 10", "5 * 10 = 50")

View file

@ -5,7 +5,7 @@ Unit tests for the Evscaperoom
import inspect import inspect
import pkgutil import pkgutil
from os import path from os import path
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia import InterruptCommand from evennia import InterruptCommand
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from evennia.utils import mod_import from evennia.utils import mod_import
@ -15,7 +15,7 @@ from . import objects
from . import utils from . import utils
class TestEvscaperoomCommands(EvenniaCommandTest): class TestEvscaperoomCommands(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.room1 = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom") self.room1 = utils.create_evscaperoom_object("evscaperoom.room.EvscapeRoom", key="Testroom")

View file

@ -3,12 +3,12 @@ Test the contrib barter system
""" """
from mock import Mock from mock import Mock
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from . import barter from . import barter
class TestBarter(EvenniaCommandTest): class TestBarter(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.tradeitem1 = create_object(key="TradeItem1", location=self.char1) self.tradeitem1 = create_object(key="TradeItem1", location=self.char1)

View file

@ -3,14 +3,14 @@ Testing clothing contrib
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from evennia.objects.objects import DefaultRoom from evennia.objects.objects import DefaultRoom
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from . import clothing from . import clothing
class TestClothingCmd(EvenniaCommandTest): class TestClothingCmd(BaseEvenniaCommandTest):
def test_clothingcommands(self): def test_clothingcommands(self):
wearer = create_object(clothing.ClothedCharacter, key="Wearer") wearer = create_object(clothing.ClothedCharacter, key="Wearer")
friend = create_object(clothing.ClothedCharacter, key="Friend") friend = create_object(clothing.ClothedCharacter, key="Friend")

View file

@ -6,7 +6,7 @@ Unit tests for the crafting system contrib.
from unittest import mock from unittest import mock
from django.test import override_settings from django.test import override_settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTestCase from evennia.utils.test_resources import BaseEvenniaTestCase
from evennia.utils.create import create_object from evennia.utils.create import create_object
from . import crafting, example_recipes from . import crafting, example_recipes
@ -655,7 +655,7 @@ class TestCraftSword(BaseEvenniaTestCase):
@mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock()) @mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock())
@mock.patch("evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe}) @mock.patch("evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe})
@override_settings(CRAFT_RECIPE_MODULES=[]) @override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftCommand(EvenniaCommandTest): class TestCraftCommand(BaseEvenniaCommandTest):
"""Test the crafting command""" """Test the crafting command"""
def setUp(self): def setUp(self):

View file

@ -4,13 +4,13 @@ Test gendersub contrib.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from mock import patch from mock import patch
from . import gendersub from . import gendersub
class TestGenderSub(EvenniaCommandTest): class TestGenderSub(BaseEvenniaCommandTest):
def test_setgender(self): def test_setgender(self):
self.call(gendersub.SetGender(), "male", "Your gender was set to male.") self.call(gendersub.SetGender(), "male", "Your gender was set to male.")
self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.") self.call(gendersub.SetGender(), "ambiguous", "Your gender was set to ambiguous.")

View file

@ -3,11 +3,11 @@ Test mail contrib
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mail from . import mail
class TestMail(EvenniaCommandTest): class TestMail(BaseEvenniaCommandTest):
def test_mail(self): def test_mail(self):
self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account) self.call(mail.CmdMail(), "2", "'2' is not a valid mail id.", caller=self.account)
self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account) self.call(mail.CmdMail(), "test", "'test' is not a valid mail id.", caller=self.account)

View file

@ -3,11 +3,11 @@ Test multidescer contrib.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import multidescer from . import multidescer
class TestMultidescer(EvenniaCommandTest): class TestMultidescer(BaseEvenniaCommandTest):
def test_cmdmultidesc(self): def test_cmdmultidesc(self):
self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:") self.call(multidescer.CmdMultiDesc(), "/list", "Stored descs:\ncaller:")
self.call( self.call(

View file

@ -9,12 +9,12 @@ import re
import itertools import itertools
from mock import Mock from mock import Mock
from evennia.utils import search from evennia.utils import search
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from . import puzzles from . import puzzles
class TestPuzzles(EvenniaCommandTest): class TestPuzzles(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super(TestPuzzles, self).setUp() super(TestPuzzles, self).setUp()
self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location) self.steel = create_object(self.object_typeclass, key="steel", location=self.char1.location)

View file

@ -4,14 +4,14 @@ Turnbattle tests.
""" """
from mock import patch, MagicMock from mock import patch, MagicMock
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from evennia.objects.objects import DefaultRoom from evennia.objects.objects import DefaultRoom
from . import tb_basic, tb_equip, tb_range, tb_items, tb_magic from . import tb_basic, tb_equip, tb_range, tb_items, tb_magic
class TestTurnBattleBasicCmd(EvenniaCommandTest): class TestTurnBattleBasicCmd(BaseEvenniaCommandTest):
# Test basic combat commands # Test basic combat commands
def test_turnbattlecmd(self): def test_turnbattlecmd(self):
@ -22,7 +22,7 @@ class TestTurnBattleBasicCmd(EvenniaCommandTest):
self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleEquipCmd(EvenniaCommandTest): class TestTurnBattleEquipCmd(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super(TestTurnBattleEquipCmd, self).setUp() super(TestTurnBattleEquipCmd, self).setUp()
self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon") self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
@ -45,7 +45,7 @@ class TestTurnBattleEquipCmd(EvenniaCommandTest):
self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleRangeCmd(EvenniaCommandTest): class TestTurnBattleRangeCmd(BaseEvenniaCommandTest):
# Test range commands # Test range commands
def test_turnbattlerangecmd(self): def test_turnbattlerangecmd(self):
# Start with range module specific commands. # Start with range module specific commands.
@ -61,7 +61,7 @@ class TestTurnBattleRangeCmd(EvenniaCommandTest):
self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") self.call(tb_range.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleItemsCmd(EvenniaCommandTest): class TestTurnBattleItemsCmd(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super(TestTurnBattleItemsCmd, self).setUp() super(TestTurnBattleItemsCmd, self).setUp()
self.testitem = create_object(key="test item") self.testitem = create_object(key="test item")
@ -78,7 +78,7 @@ class TestTurnBattleItemsCmd(EvenniaCommandTest):
self.call(tb_items.CmdRest(), "", "Char rests to recover HP.") self.call(tb_items.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleMagicCmd(EvenniaCommandTest): class TestTurnBattleMagicCmd(BaseEvenniaCommandTest):
# Test magic commands # Test magic commands
def test_turnbattlemagiccmd(self): def test_turnbattlemagiccmd(self):

View file

@ -6,7 +6,7 @@ Testing of ExtendedRoom contrib
import datetime import datetime
from mock import patch, Mock from mock import patch, Mock
from django.conf import settings from django.conf import settings
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.objects.objects import DefaultRoom from evennia.objects.objects import DefaultRoom
from . import extended_room from . import extended_room
@ -24,7 +24,7 @@ class ForceUTCDatetime(datetime.datetime):
@patch("evennia.contrib.grid.extended_room.extended_room.datetime.datetime", ForceUTCDatetime) @patch("evennia.contrib.grid.extended_room.extended_room.datetime.datetime", ForceUTCDatetime)
# mock gametime to return April 9, 2064, at 21:06 (spring evening) # mock gametime to return April 9, 2064, at 21:06 (spring evening)
@patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766)) @patch("evennia.utils.gametime.gametime", new=Mock(return_value=2975000766))
class TestExtendedRoom(EvenniaCommandTest): class TestExtendedRoom(BaseEvenniaCommandTest):
room_typeclass = extended_room.ExtendedRoom room_typeclass = extended_room.ExtendedRoom
DETAIL_DESC = "A test detail." DETAIL_DESC = "A test detail."
SPRING_DESC = "A spring description." SPRING_DESC = "A spring description."

View file

@ -3,7 +3,7 @@ Test map builder.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import mapbuilder from . import mapbuilder
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@ -187,7 +187,7 @@ EXAMPLE2_LEGEND = {
} }
class TestMapBuilder(EvenniaCommandTest): class TestMapBuilder(BaseEvenniaCommandTest):
def test_cmdmapbuilder(self): def test_cmdmapbuilder(self):
self.call( self.call(
mapbuilder.CmdMapBuilder(), mapbuilder.CmdMapBuilder(),

View file

@ -4,11 +4,11 @@ Tests of simpledoor.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . import simpledoor from . import simpledoor
class TestSimpleDoor(EvenniaCommandTest): class TestSimpleDoor(BaseEvenniaCommandTest):
def test_cmdopen(self): def test_cmdopen(self):
self.call( self.call(
simpledoor.CmdOpen(), simpledoor.CmdOpen(),

View file

@ -4,7 +4,7 @@ Slow exit tests.
""" """
from mock import Mock, patch from mock import Mock, patch
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from . import slow_exit from . import slow_exit
@ -16,7 +16,7 @@ def _cancellable_mockdelay(time, callback, *args, **kwargs):
return Mock() return Mock()
class TestSlowExit(EvenniaCommandTest): class TestSlowExit(BaseEvenniaCommandTest):
@patch("evennia.utils.delay", _cancellable_mockdelay) @patch("evennia.utils.delay", _cancellable_mockdelay)
def test_exit(self): def test_exit(self):
exi = create_object( exi = create_object(

View file

@ -3,13 +3,13 @@ Testing of TestDice.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from mock import patch from mock import patch
from . import dice from . import dice
@patch("evennia.contrib.rpg.dice.dice.randint", return_value=5) @patch("evennia.contrib.rpg.dice.dice.randint", return_value=5)
class TestDice(EvenniaCommandTest): class TestDice(BaseEvenniaCommandTest):
def test_roll_dice(self, mocked_randint): def test_roll_dice(self, mocked_randint):
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4) self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True) self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)

View file

@ -4,7 +4,7 @@ Tests for RP system
""" """
import time import time
from anything import Anything from anything import Anything
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from evennia import create_object from evennia import create_object
@ -278,7 +278,7 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(result, (Anything, self.speaker, self.speaker.key)) self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
class TestRPSystemCommands(EvenniaCommandTest): class TestRPSystemCommands(BaseEvenniaCommandTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.char1.swap_typeclass(rpsystem.ContribRPCharacter) self.char1.swap_typeclass(rpsystem.ContribRPCharacter)

View file

@ -2,12 +2,12 @@
Tutorial - talking NPC tests. Tutorial - talking NPC tests.
""" """
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from . import talking_npc from . import talking_npc
class TestTalkingNPC(EvenniaCommandTest): class TestTalkingNPC(BaseEvenniaCommandTest):
def test_talkingnpc(self): def test_talkingnpc(self):
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)") self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)")

View file

@ -6,7 +6,7 @@ Test tutorial_world/mob
from mock import patch from mock import patch
from twisted.trial.unittest import TestCase as TwistedTestCase from twisted.trial.unittest import TestCase as TwistedTestCase
from twisted.internet.base import DelayedCall from twisted.internet.base import DelayedCall
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.create import create_object from evennia.utils.create import create_object
from evennia.utils.test_resources import BaseEvenniaTest, mockdelay, mockdeferLater from evennia.utils.test_resources import BaseEvenniaTest, mockdelay, mockdeferLater
from . import mob, objects as tutobjects, rooms as tutrooms from . import mob, objects as tutobjects, rooms as tutrooms
@ -30,7 +30,7 @@ class TestTutorialWorldMob(BaseEvenniaTest):
DelayedCall.debug = True DelayedCall.debug = True
class TestTutorialWorldObjects(TwistedTestCase, EvenniaCommandTest): class TestTutorialWorldObjects(TwistedTestCase, BaseEvenniaCommandTest):
def test_tutorialobj(self): def test_tutorialobj(self):
obj1 = create_object(tutobjects.TutorialObject, key="tutobj") obj1 = create_object(tutobjects.TutorialObject, key="tutobj")
obj1.reset() obj1.reset()
@ -129,7 +129,7 @@ class TestTutorialWorldObjects(TwistedTestCase, EvenniaCommandTest):
self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack) self.call(tutobjects.CmdGetWeapon(), "", "You find Rusty sword.", obj=rack)
class TestTutorialWorldRooms(EvenniaCommandTest): class TestTutorialWorldRooms(BaseEvenniaCommandTest):
def test_cmdtutorial(self): def test_cmdtutorial(self):
room = create_object(tutrooms.TutorialRoom, key="tutroom") room = create_object(tutrooms.TutorialRoom, key="tutroom")
self.char1.location = room self.char1.location = room

View file

@ -4,9 +4,8 @@ Test the main server component
""" """
from unittest import TestCase from unittest import TestCase
from mock import MagicMock, patch, DEFAULT, call
from django.test import override_settings from django.test import override_settings
from evennia.utils.test_resources import unload_module from mock import MagicMock, patch, DEFAULT, call
@patch("evennia.server.server.LoopingCall", new=MagicMock()) @patch("evennia.server.server.LoopingCall", new=MagicMock())
@ -191,7 +190,7 @@ class TestServer(TestCase):
evennia.run_initial_setup() evennia.run_initial_setup()
acct.delete() acct.delete()
@override_settings(DEFAULT_HOME="#1") @override_settings(_TEST_ENVIRONMENT=True)
def test_run_init_hooks(self): def test_run_init_hooks(self):
from evennia.utils import create from evennia.utils import create

View file

@ -1,12 +1,34 @@
""" """
Various helper resources for writing unittests. Various helper resources for writing unittests.
Classes for testing Evennia core:
- `BaseEvenniaTestCase` - no default objects, only enforced default settings
- `BaseEvenniaTest` - all default objects, enforced default settings
- `BaseEvenniaCommandTest` - for testing Commands, enforced default settings
Classes for testing game folder content:
- `EvenniaTestCase` - no default objects, using gamedir settings (identical to
standard Python TestCase)
- `EvenniaTest` - all default objects, using gamedir settings
- `EvenniaCommandTest` - for testing game folder commands, using gamedir settings
Other:
- `EvenniaTestMixin` - A class mixin for creating the test environment objects, for
making custom tests.
- `EvenniaCommandMixin` - A class mixin that adds support for command testing with the .call()
helper. Used by the command-test classes, but can be used for making a customt test class.
""" """
import sys import sys
import re
import types
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from django.conf import settings from django.conf import settings
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from mock import Mock, patch from mock import Mock, patch, MagicMock
from evennia.objects.objects import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit from evennia.objects.objects import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
from evennia.accounts.accounts import DefaultAccount from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.scripts import DefaultScript from evennia.scripts.scripts import DefaultScript
@ -14,8 +36,14 @@ from evennia.server.serversession import ServerSession
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia.utils import create from evennia.utils import create
from evennia.utils.idmapper.models import flush_cache from evennia.utils.idmapper.models import flush_cache
from evennia.utils.utils import all_from_module from evennia.utils.utils import all_from_module, to_str
from evennia.utils import ansi
from evennia import settings_default from evennia import settings_default
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import InterruptCommand
_RE_STRIP_EVMENU = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# set up a 'pristine' setting, unaffected by any changes in mygame # set up a 'pristine' setting, unaffected by any changes in mygame
@ -242,14 +270,275 @@ class EvenniaTestMixin:
super().tearDown() super().tearDown()
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
class EvenniaCommandTestMixin:
"""
Mixin to add to a test in order to provide the `.call` helper for
testing the execution and returns of a command.
Tests a Command by running it and comparing what messages it sends with
expected values. This tests without actually spinning up the cmdhandler
for every test, which is more controlled.
Example:
::
from commands.echo import CmdEcho
class MyCommandTest(EvenniaTest, CommandTestMixin):
def test_echo(self):
'''
Test that the echo command really returns
what you pass into it.
'''
self.call(MyCommand(), "hello world!",
"You hear your echo: 'Hello world!'")
"""
# formatting for .call's error message
_ERROR_FORMAT = """
=========================== Wanted message ===================================
{expected_msg}
=========================== Returned message =================================
{returned_msg}
==============================================================================
""".rstrip()
def call(
self,
cmdobj,
input_args,
msg=None,
cmdset=None,
noansi=True,
caller=None,
receiver=None,
cmdstring=None,
obj=None,
inputs=None,
raw_string=None,
):
"""
Test a command by assigning all the needed properties to a cmdobj and
running the sequence. The resulting `.msg` calls will be mocked and
the text= calls to them compared to a expected output.
Args:
cmdobj (Command): The command object to use.
input_args (str): This should be the full input the Command should
see, such as 'look here'. This will become `.args` for the Command
instance to parse.
msg (str or dict, optional): This is the expected return value(s)
returned through `caller.msg(text=...)` calls in the command. If a string, the
receiver is controlled with the `receiver` kwarg (defaults to `caller`).
If this is a `dict`, it is a mapping
`{receiver1: "expected1", receiver2: "expected2",...}` and `receiver` is
ignored. The message(s) are compared with the actual messages returned
to the receiver(s) as the Command runs. Each check uses `.startswith`,
so you can choose to only include the first part of the
returned message if that's enough to verify a correct result. EvMenu
decorations (like borders) are stripped and should not be included. This
should also not include color tags unless `noansi=False`.
If the command returns texts in multiple separate `.msg`-
calls to a receiver, separate these with `|` if `noansi=True`
(default) and `||` if `noansi=False`. If no `msg` is given (`None`),
then no automatic comparison will be done.
cmdset (str, optional): If given, make `.cmdset` available on the Command
instance as it runs. While `.cmdset` is normally available on the
Command instance by default, this is usually only used by
commands that explicitly operates/displays cmdsets, like
`examine`.
noansi (str, optional): By default the color tags of the `msg` is
ignored, this makes them significant. If unset, `msg` must contain
the same color tags as the actual return message.
caller (Object or Account, optional): By default `self.char1` is used as the
command-caller (the `.caller` property on the Command). This allows to
execute with another caller, most commonly an Account.
receiver (Object or Account, optional): This is the object to receive the
return messages we want to test. By default this is the same as `caller`
(which in turn defaults to is `self.char1`). Note that if `msg` is
a `dict`, this is ignored since the receiver is already specified there.
cmdstring (str, optional): Normally this is the Command's `key`.
This allows for tweaking the `.cmdname` property of the
Command`. This isb used for commands with multiple aliases,
where the command explicitly checs which alias was used to
determine its functionality.
obj (str, optional): This sets the `.obj` property of the Command - the
object on which the Command 'sits'. By default this is the same as `caller`.
This can be used for testing on-object Command interactions.
inputs (list, optional): A list of strings to pass to functions that pause to
take input from the user (normally using `@interactive` and
`ret = yield(question)` or `evmenu.get_input`). Each element of the
list will be passed into the command as if the user wrote that at the prompt.
raw_string (str, optional): Normally the `.raw_string` property is set as
a combination of your `key/cmdname` and `input_args`. This allows
direct control of what this is, for example for testing edge cases
or malformed inputs.
Returns:
str or dict: The message sent to `receiver`, or a dict of
`{receiver: "msg", ...}` if multiple are given. This is usually
only used with `msg=None` to do the validation externally.
Raises:
AssertionError: If the returns of `.msg` calls (tested with `.startswith`) does not
match `expected_input`.
Notes:
As part of the tests, all methods of the Command will be called in
the proper order:
- cmdobj.at_pre_cmd()
- cmdobj.parse()
- cmdobj.func()
- cmdobj.at_post_cmd()
"""
# The `self.char1` is created in the `EvenniaTest` base along with
# other helper objects like self.room and self.obj
caller = caller if caller else self.char1
cmdobj.caller = caller
cmdobj.cmdname = cmdstring if cmdstring else cmdobj.key
cmdobj.raw_cmdname = cmdobj.cmdname
cmdobj.cmdstring = cmdobj.cmdname # deprecated
cmdobj.args = input_args
cmdobj.cmdset = cmdset
cmdobj.session = SESSIONS.session_from_sessid(1)
cmdobj.account = self.account
cmdobj.raw_string = raw_string if raw_string is not None else cmdobj.key + " " + input_args
cmdobj.obj = obj or (caller if caller else self.char1)
inputs = inputs or []
# set up receivers
receiver_mapping = {}
if isinstance(msg, dict):
# a mapping {receiver: msg, ...}
receiver_mapping = {recv: str(msg).strip() if msg else None
for recv, msg in msg.items()}
else:
# a single expected string and thus a single receiver (defaults to caller)
receiver = receiver if receiver else caller
receiver_mapping[receiver] = str(msg).strip() if msg is not None else None
unmocked_msg_methods = {}
for receiver in receiver_mapping:
# save the old .msg method so we can get it back
# cleanly after the test
unmocked_msg_methods[receiver] = receiver.msg
# replace normal `.msg` with a mock
receiver.msg = Mock()
# Run the methods of the Command. This mimics what happens in the
# cmdhandler. This will have the mocked .msg be called as part of the
# execution. Mocks remembers what was sent to them so we will be able
# to retrieve what was sent later.
try:
if cmdobj.at_pre_cmd():
return
cmdobj.parse()
ret = cmdobj.func()
# handle func's with yield in them (making them generators)
if isinstance(ret, types.GeneratorType):
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
# this mimics a user's reply to a prompt
ret.send(inp)
except TypeError:
next(ret)
ret = ret.send(inp)
else:
# non-input yield, like yield(10). We don't pause
# but fire it immediately.
next(ret)
except StopIteration:
break
cmdobj.at_post_cmd()
except StopIteration:
pass
except InterruptCommand:
pass
for inp in inputs:
# if there are any inputs left, we may have a non-generator
# input to handle (get_input/ask_yes_no that uses a separate
# cmdset rather than a yield
caller.execute_cmd(inp)
# At this point the mocked .msg methods on each receiver will have
# stored all calls made to them (that's a basic function of the Mock
# class). We will not extract them and compare to what we expected to
# go to each receiver.
returned_msgs = {}
for receiver, expected_msg in receiver_mapping.items():
# get the stored messages from the Mock with Mock.mock_calls.
stored_msg = [
args[0] if args and args[0] else kwargs.get("text", to_str(kwargs))
for name, args, kwargs in receiver.msg.mock_calls
]
# we can return this now, we are done using the mock
receiver.msg = unmocked_msg_methods[receiver]
# Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [str(smsg[0])
if isinstance(smsg, tuple) else str(smsg) for smsg in stored_msg]
if expected_msg is None:
# no expected_msg; just build the returned_msgs dict
returned_msg = "\n".join(str(msg) for msg in stored_msg)
returned_msgs[receiver] = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
else:
# compare messages to expected
# set our separator for returned messages based on parsing ansi or not
msg_sep = "|" if noansi else "||"
# We remove Evmenu decorations since that just makes it harder
# to write the comparison string. We also strip ansi before this
# comparison since otherwise it would mess with the regex.
returned_msg = msg_sep.join(
_RE_STRIP_EVMENU.sub(
"", ansi.parse_ansi(mess, strip_ansi=noansi))
for mess in stored_msg).strip()
# this is the actual test
if expected_msg == "" and returned_msg or not returned_msg.startswith(expected_msg):
# failed the test
raise AssertionError(
self._ERROR_FORMAT.format(
expected_msg=expected_msg, returned_msg=returned_msg)
)
# passed!
returned_msgs[receiver] = returned_msg
if len(returned_msgs) == 1:
return list(returned_msgs.values())[0]
return returned_msgs
# Base testing classes
@override_settings(**DEFAULT_SETTINGS) @override_settings(**DEFAULT_SETTINGS)
class BaseEvenniaTestCase(TestCase): class BaseEvenniaTestCase(TestCase):
""" """
Base test (with no default objects) but with Base test (with no default objects) but with enforced default settings.
enforced default settings.
""" """
class EvenniaTestCase(TestCase):
"""
For use with gamedir settings; Just like the normal test case, only for naming consistency.
"""
pass
@override_settings(**DEFAULT_SETTINGS) @override_settings(**DEFAULT_SETTINGS)
class BaseEvenniaTest(EvenniaTestMixin, TestCase): class BaseEvenniaTest(EvenniaTestMixin, TestCase):
@ -258,7 +547,6 @@ class BaseEvenniaTest(EvenniaTestMixin, TestCase):
""" """
class EvenniaTest(EvenniaTestMixin, TestCase): class EvenniaTest(EvenniaTestMixin, TestCase):
""" """
This test class is intended for inheriting in mygame tests. This test class is intended for inheriting in mygame tests.
@ -273,3 +561,28 @@ class EvenniaTest(EvenniaTestMixin, TestCase):
exit_typeclass = settings.BASE_EXIT_TYPECLASS exit_typeclass = settings.BASE_EXIT_TYPECLASS
room_typeclass = settings.BASE_ROOM_TYPECLASS room_typeclass = settings.BASE_ROOM_TYPECLASS
script_typeclass = settings.BASE_SCRIPT_TYPECLASS script_typeclass = settings.BASE_SCRIPT_TYPECLASS
@patch("evennia.commands.account.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.admin.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.batchprocess.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.building.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.comms.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.general.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.help.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.syscommands.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.system.COMMAND_DEFAULT_CLASS", MuxCommand)
@patch("evennia.commands.unloggedin.COMMAND_DEFAULT_CLASS", MuxCommand)
class BaseEvenniaCommandTest(BaseEvenniaTest, EvenniaCommandTestMixin):
"""
Commands only using the default settings.
"""
class EvenniaCommandTest(EvenniaTest, EvenniaCommandTestMixin):
"""
Parent class to inherit from - makes tests use your own
classes and settings in mygame.
"""

View file

@ -4,10 +4,10 @@ Test eveditor
""" """
from evennia.utils import eveditor from evennia.utils import eveditor
from evennia.commands.default.tests import EvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
class TestEvEditor(EvenniaCommandTest): class TestEvEditor(BaseEvenniaCommandTest):
def test_eveditor_view_cmd(self): def test_eveditor_view_cmd(self):
eveditor.EvEditor(self.char1) eveditor.EvEditor(self.char1)
self.call( self.call(