CardFRP Unit Testing
Introduction
The unit testing for the CardFRP component is intended to exercise all
non-trivial methods in the basic CardFRP classes
(GameObject, GameActor, GameContext, GameAction, and Dice).
This testing will be broken into two parts:
- Each basic class will have its own basic unit test suite in the same
module as the implementation. All of these test cases will be automatically
run out of the __main__() method, and will assert() to
check the correctness of each test-case.
This is a stardard model for technique for Python class unit-testing, and
has the advantage of keeping the per-method test cases tightly bundled with
the code they exercise (and ensuring that they always match).
- More interesting test cases involve operations that combine the
capabilities of multiple classes. These will be implemented by a
PyTest test suite.
In addition to the above pass/fail test cases, we will also provide sample
code to exercise a wide range of common game scenarios (e.g. searches,
interactions, combat, scrolls).
Per-Class Unit Testing
GameObject Unit Testing
The most interesting code in the GameObject class is the
possible_actions() and accept_action() methods.
The test cases to exercise them are combinations of situations to
exercise different paths through the code ... and so should be
regarded as white box test cases.
The possible_actions() functionality to be exercised is:
- a newly created GameObject returns no actions and
has no ACCURACY, DAMAGE, POWER or STACKS attributes
- if the ACTIONS attribute is a single, or a list of multiple
(simple or compound) verbs, possible_actions() will
return a list containing only (and all of) those verbs
- for each ATTACK verb in ACTIONS, it will correctly compute
ACCURACY and DAMAGE by adding the GameObjects
base-verb and subtype ACCURACY and DAMAGE attributes
- for each non-ATTACK verb in ACTIONS, it will correctly compute
POWER and STACKS by adding the GameObjects
base-verb and subtype POWER and STACKS attributes
- for a compound action (multiple verbs separated by +)
the ACCURACY, DAMAGE, POWER and STACKs attributes will
each be a list, with the correct number of entries,
and the correct values in each slot (in the same order
as the verbs)
A set of (specification based, black-box) test cases to exercise this functionality would be:
- create a new GameObject, get its possible_actions,
and confirm the emptiness of the list and the absence of any
ACCURACY/DAMAGE/POWER/STACKS attributes.
- create a new GameObject, with a single ATTACK verb
and only base-verb ACCURACY and DAMAGE,
call possible_actions(), and confirm that the
returned list of GameActions contains only that verb
with those attributes.
- create a new GameObject, with a multiple ACTIONS,
call possible_actions(),
and confirm that correct (verb and attributes) GameActions
are returned for each.
- create a new GameObject, with multiple compound
verbs (both ATTACK and non-ATTACK), some of which have
sub-type (in addition to base-verb) attributes,
call possible_actions() and
- confirm that the ACCURACY/DAMAGE/POWER/STACKS attributes
of the returned GameActions
contain the propper values (sums of base and sub-type
attributes, in the same order as their corresponding verbs)
- confirm the entries for which there should be no values
(e.g. the third entry in the POWER list when the third verb
was an ATTACK) are 0.
The accept_action() method in this base class only knows
how to handle attribute-changing verbs. ATTACK verbs are handled
by the GameActor sub-class.
The functionality to be exercised is:
- proper summation of general RESISTANCE, and the verb-appropriate
RESISTANCE.verb, and RESISTANCE.verb.subtype attributes.
- proper iteration through the incoming STACKS, and proper
comparison of TO_HIT and RESISTANCE+D100 to determine whether
or not a given STACK of the action is delivered.
- proper update of receiver attributes in response to successful
delivery of (both positive and negative) actions.
- ensuring that LIFE cannot be raised above HP, by doing
multiple (guaranteed to succeed) actions that would
raise it more than possible.
- success is returned only if some STACKS are delivered
- returned status message correctly reflects complete RESISTANCE or
the number of STACKS successfully delivered
There is a little complexity to these rules, and constructing a set of
test cases (based on an understanding of these requirements and their
interactions) to well-exercise their correct implementation might
reasonably be considered to cross the line into white-box testing:
- pass actions for which there are various combinations
of base, verb, and sub-type RESISTANCE (in values that
will cause the action to succeed or fail based on which
terms are included) and confirm that each action succeeds
or fails appropriately.
- pass actions with large numbers of STACKS (some guaranteed to
all succeed, some guaranteed to all fail, and some succeeding
or failing based on the roll), and confirm
- that success return value is true if any STACKS were delivered
- (by parsing the status) that the number blocked is proportional to
(D100 + RESISTANCE - TO_HIT)/100
- that the affected attributes have been
correctly updates
- that it is impossible to raise LIFE above HP, even
if enough STACKS to do so get through
The load() method, which draws on other classes, will be
exercised (for both GameActors and GameContexts
in the game PyTest tests.
GameAction Unit Testing
The interesting code is in the act() method, where we
recognize and split up compound verbs, decide which (object and
initiator, (base and sub-type) ACCURACY, DAMAGE, POWER and STACKS
attributes apply to each, compute TO_HIT and TOTAL attributes,
and pass these to the recipient's accept_action() method.
The functionality to be exercised is:
- breaking compound (separated by +) verbs into distinct
(single-verb) actions, delivered to the recipient,
in the correct order
- correct selection and combination of the action's base,
verb, and sub-type ACCURACY/POWER attributes with any
corresponding initiator bonuses to compute the TO_HIT
for each delivered action
- correct selection and combination of the action's base,
verb, and sub-type DAMAGE/STACKS attributes with any
corresponding initiator bonuses to compute the TOTAL
for each delivered action
- ensuring that those correctly calculated values are
received by the recipient's accept_action() method
Again, the construction of a set of test cases to exercise the
correct handling of compound verbs might reasonably be considered
to cross the line into white box testing:
- create GameActions with a simple ATTACK, and
multiple sub-types, each of which has specified
(base) ACCURACY and DAMAGE attributes.
Confirm that the delivered GameAction
has the correct verb, TO_HIT and TOTAL attributes.
- create GameActions with a simple ATTACK, and
multiple sub-types, each of which has specified
(base) ACCURACY and DAMAGE attributes.
Create an initiator who has base ACCURACY and DAMAGE
bonuses, as well as sub-type bonuses for some
(but not all) of the verbs.
Confirm that the delivered GameAction
has the correct verb, TO_HIT and TOTAL attributes.
- create GameActions with a simple MENTAL action,
and multiple sub-types, each of which has specified
(base) POWER and STACKS attributes.
Confirm that the delivered GameAction
has the correct verb, TO_HIT and TOTAL attributes.
- create GameActions with a simple MENTAL action,
and multiple sub-types, each of which has specified
(base) POWER and STACKS attributes.
Create an initiator who has base POWER and STACKS
bonuses, as well as sub-type bonuses for some
(but not all) of the verbs.
Confirm that the delivered GameAction
has the correct verb, TO_HIT and TOTAL attributes.
- create a GameAction with a compound action
involving both ATTACK and MENTAL/VERBAL/PHYSICAL verbs,
with ACCURACY, DAMAGE, POWER, and STACKS attributes
that contain lists of values (corresponding to each
of the verbs in the compound action).
Call act() to deliver those actions, and
confirm that each of the test recipient received
each of those verbs with its correct TO_HIT and STACKS
attributes.
- create a GameAction with a compound action,
where one of the later actions will fail (POWER=0).
Call act() to deliver those actions, and
confirm that no verbs after the failure were delivered
to the recipient.
In support of the last two, we will create a test-recipient, who's
accept_action method simply returns (as status) the verb
and attribute values. The test functions can parse this output to
confirm that the received GameAction had the expected
verb, TO_HIT and TOTAL attributes, in the correct order.
The test cases to exercise this are combinations of situations to
exercise different paths through the code ... and so should be
regarded as white box test cases.
Dice Unit Testing
The Dice class is simple and independent, and so should be
very easy to test. There are two obvious types of tests:
- (black-box) tests of each type of legal formula,
to make sure that the (a) are accepted and (b) that they result
in correct rolls.
- tests of likely illegal formula, to make sure that
they are rejected.
The easiest way to test correct rolling is to do many more rolls
than the range width, and ensure that all returned rolls are
within the expected range. I will write a routine:
test(
string formula, # dice roll formula
int min_expected, # lowest legal return value
int max_expected, # highest legal return value
int num_rolls) # number of rolls to test
that will do this, and use it to test each of the basic types of roll formula:
| formula |
min expected |
max expected |
num rolls |
| "3D4" | 3 | 12 | 40 |
| "d20" | 1 | 20 | 80 |
| "D%" | 1 | 100 | 300 |
| "2D2+3" | 5 | 7 | 7 |
| "3-9" | 3 | 9 | 9 |
| "47" | 47 | 47 | 10 |
| 47 | 47 | 47 | 10 |
| "-3" | -3 | -3 | 10 |
The obvious formula errors are typos and missing values:
- missing number of faces (e.g. "2D")
- non-numeric limits, number of dice, or number of faces (e.g. "D", "xDy", "x-y")
- ranges where the last number is missing or lower than the first (e.g. "-", "4-2", "3-")
- ranges using a separator other than "-" (e.g. "7 to 9")
The above are merely obvious examples the supported expression formats and
obvious errors. As such, these would probably be considered to be
black box test cases.
GameActor Unit Testing
By far, the most interesting code in GameActor is the ATTACK handling
in the accept_action() method. The obvious things to check are:
- confirming correct comparison of incoming TO_HIT vs general, base-verb,
and sub-type EVASION
- confirming correct use of D100 rolls to determine whether or
not an ATTACK succeeds
- confirming correct comparison of incoming TOTAL damage vs general,
base-verb, and sub-type PROTECTION
- confirming correct updates to the LIFE, alive,
and disabled in response to received damage
The simplest tests of attribute comparison and LIFE update might be:
- initiating an attack that (due to poor ACCURACY) is sure
to fail, and confirming that it fails and has no effect
on the recipient's LIFE
- initiating an attack that (due to excellent EVASION) is sure
to fail, and confirming that it fails and has no effect
on the recipient's LIFE
- initiating an attack that would only fail if both the
base and sub-type EVASION were included, and confirming
that it does indeed fail with no effect on LIFE.
- initiating an attack that (due to excellent ACCURACY and
excellent PROTECTION) is is sure to land, but will deliver
no damage, and confirming that it succeeds but has no effect
on the recipient's LIFE
- initiating an attack that (due to perfect ACCURACY, and
no PROTECTION) is sure to succeed, and confirming that
it succeeds and correctly reduces the recipient's LIFE
- initiating an attack that is sure to succeed, but
have its damage reduced by the combination of base
and sub-typte PROTECTION, and confirming that the
correct reduction in lost LIFE points.
Again, while the above break-down of cases could be done on the basis
of the specifications, it represents deeper and more complete analysis
"make sure each function does what it is specified to do". As such,
this too might reasonably be considered to be a set of black box
test cases.
Non-ATTACK (condition affecting) actions are not implemented in this
class, but forwarded to our underlying GameObject super-class.
Even though those actions are not actually processed in this class,
we should still confirm that they are correctly forwarded and have
the expected effects on our attributes, based on
our RESISTANCEs:
- initiating an action that (due to poor POWER) is sure
to fail, and confirming that it has no effect on the
associated attribute
- initiating an action that (due to excellent RESISTANCE)
is sure to to fail, and confirming no change to the
names attribute
- initiating an action that would only fail if both the
base and sub-type RESISTANCE were included, and confirming
that it does indeed fail with no effect on the associated
attribute
- initiating an action that (because we have no RESISTANCE)
is sure to deliver all of its STACKS, and confriming that
the associated attribute has gone up by the number of
STACKS
We can test the use of dice rolls:
- initiating an ATTACK that (as a result of 0 ACCURACY and
50% EVASION) should get through (roughly) half the time,
performing it ten times, and confirming (by both success
returns and LIFE attribute updates) that it got through
some of the time and failed some of the time
- initiating an MENTAL action that (as a result of 0 POWER and
50% RESISTANCE) should get through (roughly) half the time,
performing it ten times, and confirming (by both success
returns and updates to the associated attribute) that it
got through some of the time and failed some of the time
GameContext Unit Testing
The GameContext implementation is, for the most part, a trivial extension
of the base GameObject class, and is easily exercised with a few
black-box tests:
- testing the add_member(), remove_member()
and get_party() methods can be done by starting
with an empty GameContext, and doing a series of
add_member() and remove_member() calls
(in different combinations), confirming after each that
get_party() returns the expected list.
- testing the NPC methods is the same: start with an empty
GameContext, and do a series of add_npc()
and remove_npc() calls (in different combinations),
confirming after each that get_npcs() returns
the expected list.
- the hierarchical get() functionality can be tested by
creating a context, a parent, and a grand-parent which define
overlapping and distinct attributes, and confirming that
get() operations on the lowest child correctly return
the most proximate value for the requested attribute.
- the SEARCH action can be created by creating some test objects
(that will resist a certain number of SEARCH operations), doing
a series of SEARCHes, and confirming that each successive
SEARCH discovers the expected objects.
Whole Component Test Scenarios
Some of the obvious unit test cases involve the interactions of multiple
classes, and so I have chosen to put these into the (PyTest)
test_cardfrp.py module. These include:
- loading a simple GameActor description from a file
and confirming the correct setting of name, description,
and a few attributes
- loading a complex GameActor description (that includes
enabled actions and posessed objects) from a file, and confirm
that they were all correctly instantiated
- loading a GameContext description that includes both
visible and hidden objects from a file, and confirm their presence,
and that (sufficient) SEARCH operations will discover the hidden
objects
- having one GameActor obtain a list of possible
interactions from another, and exercise them,
confirming that those with sufficient POWER or STACKS
succeed and have the intended effects
In addition to these "does it work" test cases, there is a
scenarios.py that provides demonstrations of fully
integrated functionality:
- all possible actions performed in the local context
- all possible non-COMBAT actions performed with an NPC
- combat (to the death) with a group of NPCs
- compound attack weapon use (e.g. a poisoned dagger)
- the use and efficacy of magical artifacts (e.g. scrolls
and potions): cure Light Wounds, and Courage