Skip to content

Latest commit

 

History

History
101 lines (68 loc) · 5.58 KB

README.md

File metadata and controls

101 lines (68 loc) · 5.58 KB

Ambassador Unit Tests

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 Ambassador Module and such;
  • a MappingTest defines a Mapping that will be added to both the Plain and TCP AmbassadorTests;
  • an OptionTest defines an option that will be added to all MappingTests; 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.

OptionTest

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 just yield the string you want to be added to the Mapping configuration for your option
  • the queries method only needs to yield queries that the parent MappingTest won't already be doing. So, for example, the AddRequestHeader test doesn't have a queries method -- it will have an effect on all the queries that the parent is already performing.
  • the check method can assert about self.parent.results to check things about queries made by the parent, or about self.results if the queries method added more queries.

MappingTest

A MappingTest adds an entirely new Mapping to an AmbassadorTest's Ambassador. MappingTests are a bit more challenging to work with than OptionTests.

  • To initialize a MappingTest, you must pass it a target, which must be an instance of (a subclass of) ServiceType at minimum, and you may also pass it a tuple of OptionTests (and of course a name). The minimal instantiation is something like

    MappingTest(HTTP())

    to create a MappingTest with our generic HTTP echo service for its target. The target is accessible in the MappingTest as self.target.

  • The variants class method must yield valid instances of the MappingTest. The WebSocketMapping 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 of MappingTest. It varies with ServiceTypes -- for every kind of ServiceType out there, we take the instantiated ServiceType and instantiate a new WebSocketMapping with the instantiated ServiceType as the target. We need to change the name of our new instance, too, so that we don't have duplicates.

  • queries and check should generate and check a new set of queries. Again, it's OK to look at self.parent.results in check.

AmbassadorTest

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 an ambassador_id of its name. You can override this by setting ambassador_id on your instance, but why bother?

  • By default, an AmbassadorTest will appear in namespace default. You can override this by setting namespace 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 to yield tuples: yield self, configstr to set configuration on the Ambassador itself, or yield self.target, configstr to set configuration on a service you've saved as self.target.

  • If you need to change manifests, you will almost certainly need to include a call to super().manifests() too. The common pattern here is

    def 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 as self.target in the init method (not __init__ -- leave that alone!).

More coming soon!