This draws a nodes-and-edges diagram of the relationships between a set of object instances in a python program, to help you check whether your test setup code is correct.
It uses dot
from Graphviz to render the diagram to SVG, and display
from
ImageMagick to display it.
Here is the code to generate the diagram above:
from typing import List, NamedTuple
class SaleOrder(NamedTuple):
reference: str
sku: str
lines: List[str]
class SaleOrderLine(NamedTuple):
name: str
quantity: str = 1
class Product(NamedTuple):
sku: str
entities = [
SaleOrder(reference="123",
sku="abc",
lines=[SaleOrderLine("first"),
SaleOrderLine("second")]),
Product(sku="abc")
]
config = fixturegraph.Configuration(
id_attrs={
SaleOrder: "reference",
Product: "sku",
SaleOrderLine: "name",
},
attrs_with_child_refs={
SaleOrder: ["lines"],
},
)
fixturegraph.show_diagram(config, entities)
The intended use case is for testing your understanding of test fixtures you've made when writing test cases. In other words, it's for testing your understanding of your test setup code. Typically, in Domain Driven Design (DDD) terminology, the objects involved are "entities". Entities are just objects with an identity -- usually the identity is a string. fixture-graph labels the nodes in the diagram with a name formed from the type of the object and its identity -- e.g. "Widget_1". If the objects you're interested in aren't really entities, you can still include them in the diagram as long as you nominate some attribute to use as the identity to use in the label.
Sometimes you think you've set up one situation in a test when you've actually
set up a different one: I thought I had this Widget
hooked up to these two
Lozenge
s, but actually one of the Lozenge
s is sitting there on its own not
connected to anything. That often causes a false positive test result (test
passes when it shouldn't), or a false negative (test fails when it shouldn't).
I've found it can sometimes take me a long time to figure out what I did wrong,
or worse maybe I never even notice -- and then I've tested something different
to what I thought I had.
I hope that looking at these diagrams saves time and frustration when trying to understand test setup code, and perhaps even helps avoid bugs in production code.
The goal is to provide a way to sometimes tell more quickly that something is wrong, without providing quite as much information as a full dump of the object graph with all its python references and implicit string identifier relationships (see next section). It's not intended to show you everything in full detail.
Right now I find I mostly just pretty-print entities rather than looking at
these charts, but they have sometimes been useful. I recommend making
pretty-printing very easy to do in your project: for example, you can add a
pytest fixture that returns a no-arguments function that prints all entities
created in your test. PyPI module prettyprinter
is good for this.
One common way to write the wrong test in this way is to fail to hook up two objects that are related only by string identifiers (or tuples of strings / lists of strings). This indirect relationship is common in code that uses Domain Driven Design patterns, where entities from different aggregates may be related in this way rather than with explicit python references from one object to another. I make these same mistakes with my test setup code whether I'm writing code this way or not, but not having an explicit relationship makes it a bit harder for a tool like this: what arrows to draw?
This code supports that (as well as explicit python object references) by drawing an arrow between two object instances when they have attributes with the same string value.
If two objects have attributes that are tuples or lists of strings, the string
items in those sequences count simply as more attributes: the whole tuple need
not match another tuple for an arrow to be drawn, rather it's enough for any one
item to match. If a sequence attribute is named foo
, the items in the
sequence are named foo_0
, foo_1
, etc. in the diagram.
To make the diagram less busy, the arrows taken from your objects' attributes are combined into fewer arrows actually drawn in the dot diagram. Currently, these are the rules used to do that:
-
If there are inferred arrows sharing the same value, they are combined into one arrow, which is labelled with the names of all the attributes involved (if more than one attribute has the same name, that name is only listed once).
-
Combine remaining arrows that share a directed edge. Example: if Laptop1 has attributes disk and storage both pointing to Disk1, they will be combined into a single arrow labelled with both attribute names.
To use this you need Graphviz (the dot
program in particular) and ImageMagick
(the display
program in particular).
To install fixture-graph:
pip install git+https://github.com/jlee1-made/fixture-graph.git@master#egg=fixturegraph
Here's a simple running example:
import typing
class SaleOrder(typing.NamedTuple):
reference: str
sku: str
class Product(typing.NamedTuple):
sku: str
entities = [SaleOrder(reference="123", sku="abc"), Product(sku="abc")]
config = fixturegraph.Configuration(
id_attrs={
SaleOrder: "reference",
Product: "sku",
},
)
fixturegraph.show_diagram(config, entities)
Here's an incomplete example showing how to use it in the form of a pytest fixture together with some common patterns used in DDD projects (Repository, Unit of Work):
@pytest.fixture
def dot(unit_of_work):
def dot():
print()
entities = []
for repository_name in [
"sale_orders",
"products",
"stock",
]:
repo = getattr(unit_of_work, repository_name)
entities.extend(repo.all())
for entity in entities:
if isinstance(entity, SaleOrder):
entities.extend([l for l in entity.lines])
config = fixturegraph.Configuration(
id_attrs={
SaleOrder: "get_aggregate_id",
SaleOrderLine: "reference",
Product: "aggregate_id",
Stock: "get_aggregate_id",
},
# The following are optional, only id_attrs is required:
synonymous_attrs={
Stock: ["reference", "warehouse_code", "warehouse"],
},
attrs_with_child_refs={
SaleOrder: ["lines"],
}
)
fixturegraph.show_diagram(config, entities)
return dot
and then in a test:
def test_something(dot):
so = make_sale_order_with_two_lines()
dot()
Note that the values of the attributes named by id_attrs
can be either strings
or no-arguments getter methods that return a string.
You can pass optional config in synonymous_attrs
and attrs_with_child_refs
to Configuration if you like. synonymous_attrs
will hide all but one of the
arrow labels for multiple attributes are present that are really just the same
thing under different names. attrs_with_child_refs
will draw arrows for
attributes you list there that are explicit python references (or lists/tuples
of explicit python references). Both are dicts like id_attrs
that map classes
to attribute names.
Q. You're doing it all wrong, you should use public interfaces to construct your test fixtures and then they'll always be correct
A. I tend to agree with this advice, but it's still possible to end up with a scenario that's not what you intended.
To run mypy, in the top level directory of the git project, run:
./run_mypy.sh
To run the tests, in the top level directory of the git project, run:
./run_tests.sh
I may well never do any of this but:
-
You have to specify an "id" even for things that in DDD terminology are non aggregate roots -- here you can just provide a name rather than an id. That's weird and it could result in accidentally linking nodes that should not be linked.
-
Make the rules used to combine arrows controllable from command line. Possibly by entity type as well as just for all arrows?
-
The graphs get complicated quickly as the number of nodes rises. Provide better ways to simplify / focus on particular areas. Of course you can already do that yourself by just not passing every entity in your test to show_diagram.
-
Allow configuring how to deal with private attributes
-
Allow overriding tool used to display the diagram, and what format to render it in (SVG, PNG, etc.)
-
Provide a way to automatically discover what attributes have direct python references to other entities in the list passed to fixturegraph.show_diagram, instead of having to list the names of those attributes in
Configuration.attrs_with_child_refs