Ambassador is tested with KAT. You are strongly encouraged to add tests when you add features. However, KAT is fairly complex, and how you actually add will depend a bit on what kind of thing you're adding. Here we'll talk a bit about how Ambassador uses KAT, and how to work within that
The Ambassador KAT class hierarchy looks a bit like this:
Node
ServiceType (abstract_tests.py)
HTTP -- generic HTTP echo backend (abstract_tests.py)
AHTTP -- authentication service using HTTP (abstract_tests.py)
etc.
Test
AmbassadorTest (abstract_tests.py)
TCP (test_ambassador.py)
Plain (test_ambassador.py)
TracingTest (t_tracing.py)
etc.
MappingTest (abstract_tests.py)
SimpleMapping (test_ambassador.py)
OptionTest (abstract_tests.py)
AddRequestHeaders (test_ambassador.py)
AddResponseHeaders (test_ambassador.py)
etc.
Broadly speaking:
- an
AmbassadorTest
is a test that instantiates its own Ambassador, and can therefore safely modify things like the AmbassadorModule
and such; - a
MappingTest
defines aMapping
that will be added to both thePlain
andTCP
AmbassadorTests
; - an
OptionTest
defines an option that will be added to allMappingTests
; and - a
ServiceType
is a kind of backend service you can talk to.
Historically, we started with all the tests lumped into test_ambassador.py
. As this has become more and more unwieldy, we've split out into the t_*.py
files. If you want to create a new file for your test, great! but don't give it a name starting with test_
because that can confuse things.
All the way at the bottom of test_ambassador.py
you'll find
main = Runner(AmbassadorTest)
which is what actually uses KAT to instantiate and run all subclasses of AmbassadorTest
as tests.
If you're lucky, your test can be an OptionTest
: this is appropriate if you're adding an option to the Mapping
class. Just check out an existing test (like AddRequestHeaders
) and you'll be good to go.
Within an OptionTest
:
- the
config
method should justyield
the string you want to be added to theMapping
configuration for your option - the
queries
method only needs toyield
queries that the parentMappingTest
won't already be doing. So, for example, theAddRequestHeader
test doesn't have aqueries
method -- it will have an effect on all the queries that the parent is already performing. - the
check
method can assert aboutself.parent.results
to check things about queries made by the parent, or aboutself.results
if thequeries
method added more queries.
A MappingTest
adds an entirely new Mapping
to an AmbassadorTest
's Ambassador. MappingTest
s are a bit more challenging to work with than OptionTest
s.
-
To initialize a
MappingTest
, you must pass it atarget
, which must be an instance of (a subclass of)ServiceType
at minimum, and you may also pass it a tuple ofOptionTest
s (and of course aname
). The minimal instantiation is something likeMappingTest(HTTP())
to create a
MappingTest
with our generic HTTP echo service for itstarget
. Thetarget
is accessible in theMappingTest
asself.target
. -
The
variants
class method mustyield
valid instances of theMappingTest
. TheWebSocketMapping
test is a good example:class WebSocketMapping(MappingTest): ... @classmethod def variants(cls): for st in variants(ServiceType): yield cls(st, name="{self.target.name}")
Here we define
WebSocketMapping
as a subclass ofMappingTest
. It varies withServiceType
s -- for every kind ofServiceType
out there, we take the instantiatedServiceType
and instantiate a newWebSocketMapping
with the instantiatedServiceType
as thetarget
. We need to change thename
of our new instance, too, so that we don't have duplicates. -
queries
andcheck
should generate and check a new set of queries. Again, it's OK to look atself.parent.results
incheck
.
An AmbassadorTest
is the place that new Ambassadors actually get created. If your test doesn't need a new Ambassador, don't create a new AmbassadorTest
! Each of these requires a new pod in the test cluster; they're somewhat expensive. On the other hand, you need an AmbassadorTest
if you need to play with the Ambassador
Module
, or TLS contexts, or things like that.
-
By default, an
AmbassadorTest
uses anambassador_id
of its name. You can override this by settingambassador_id
on your instance, but why bother? -
By default, an
AmbassadorTest
will appear in namespacedefault
. You can override this by settingnamespace
on your instance. -
If you set
single_namespace
on your instance, that will restrict your Ambassador to only looking in its namespace. -
The
config
method will likely need toyield
tuples:yield self, configstr
to set configuration on the Ambassador itself, oryield self.target, configstr
to set configuration on a service you've saved asself.target
. -
If you need to change
manifests
, you will almost certainly need to include a call tosuper().manifests()
too. The common pattern here isdef manifests(self): return manifeststr + super().manifests()
so that you can be that the manifest in
manifeststr
takes effect before the Ambassador is created. -
A fairly common pattern for a single-purpose
AmbassadorTest
is to create a target asself.target
in theinit
method (not__init__
-- leave that alone!).