Sample Tester¶
The sample tester allows defining tests once and applying them to semantically identical executables (typically runnable code samples) instantiated in multiple languages and environments.
Installation¶
(optional) Activate your preferred virtual environment:
. PATH/TO/YOUR/VENV/bin/activate
Install the necessary packages:
pip install pyyaml # needs to be installed before sample-tester
pip install sample-tester
This will put the command sample-tester
in your path.
Defining tests¶
To execute a test, you will need:
A “test plan”, defined via one or more
*.yaml
files. Here’s an example:language.test.yaml¶test: suites: - name: "Language samples test" setup: # can have yaml and/or code, just as in the cases below - code: log('In setup "hi"') teardown: # can have yaml and/or code, just as in the cases below - code: log('In teardown bye') cases: - name: "A test defined via yaml directives" spec: - log: - 'Reading from manifest at {language_analyze_sentiment_text:@manifest_source}' - call: sample: "language_analyze_sentiment_text" params: content: literal: "happy happy smile hope" - assert_success: [] # try assert_failure to see how failure looks - assert_contains: - message: "Have score and magnitude" - literal: "score" - literal: "magnitude" - assert_contains_any: - message: "Have magnitude or strength" - literal: "strength" - literal: "magnitude" - assert_contains: - message: "Score is very positive" - literal: "score: 0.8" - assert_contains: - message: "Magnitude is very positive" - literal: "magnitude: 0.8" - assert_excludes: - message: "Random message" - literal: "The rain in Spain falls mainly in the plain" - assert_not_contains: # deprecated: use "assert_excludes" instead - message: "Random message" - literal: "The rain in Spain falls mainly in the plain"
See the Testplan page for information on the yaml directives available in the testplan, and how to use them directly via embedded Python code.
A “manifest”, defined via one or more
*.manifest.yaml
files. Here’s an example:language.manifest.yaml¶--- type: manifest/samples schema_version: 3 samples: - environment: java invocation: "{jar} -D{class} {path} @args" path: "examples/mock-samples/java/language-v1/AnalyzeSentiment" class: AnalyzeSentiment jar: "./do_java" chdir: "examples/mock-samples/java/" sample: "language_analyze_sentiment_text" - environment: python bin: "python3" path: "examples/mock-samples/python/language-v1/analyze_sentiment_request_language_sentiment_text.py" sample: "language_analyze_sentiment_text" - environment: bash # notice: no "bin:" because artifacts are already executable path: "examples/mock-samples/sh/language-v1/analyze_sentiment.sh" sample: "language_analyze_sentiment_text"
See the Manifest file format page for an explanation of the manifest.
Testplan¶
One of the inputs to sample-tester is the “testplan”, which outlines how to run the samples and what checks to perform.
The testplan can be spread over any number of
TESTPLAN.yaml
files.You can have any number of test suites.
Each test suite can have
setup
,teardown
, andcases
sections.The
cases
section is a list of test cases. For _each_ test case,setup
is executed before running the test case andteardown
is executed after.setup
,teardown
and eachcases[...].spec
is a list of directives and arguments. The directives can be any of the following YAML directives:log
: print the arguments, printf style.Substrings of the form
{}
are interpolated with the corresponding positional arguments specifiedSubstrings of the form
{id:name}
are substituted with the value of the manifest tag corresponding to the keyname
for the sample identified byid
. This can be useful when debugging your test to make sure that all the tags are as you expect.id
must resolve to a sample ID specified in the manifest file- if
name
does not match any tag key for the sampleid
, the substring is substituted with the empty string - if
name
is not specified, the substring is substituted with a serialized representation of all the tags specified for the sampleid
uuid
: return a uuid (if called from yaml, assign it to the variable names as an argument)shell
: run in the shell the command specified in the argumentcall
: call the artifact named in the argument; error if the call failscall_may_fail
: call the artifact named in the argument; do not error even if the call failsassert_contains
: require the output of the lastcall*
to contain all of the strings provided (case-insensitively); abort the test case otherwiseassert_excludes_all
(and the previous deprecated formassert_not_contains
): require the output of the lastcall*
to not contain any of the strings provided (case-insensitively); abort the test case otherwiseassert_contains_any
: require the output of the lastcall*
to contain at least one of the strings provided (case-insensitively); abort the test case otherwiseassert_excludes_any
: require the output of the lastcall*
to not contain at least one of the strings provided (case-insensitively); abort the test case otherwiseassert_success
: require that the exit code of the lastcall_may_fail
was 0; abort the test case otherwise. If the preceding call was a just acall
, it would have already failed on a non-zero exit code.assert_failure
: require that the exit code of the lastcall_may_fail
orcall
was NOT 0; abort the test case otherwise. Note, though, that if we’re executing this after just acall
, it must have succeeded so this assertion will fail.env
: assign the value of an environment (identified byvariable
) variable to a test case variable (given byname
)extract_match
: extrack regex matches into local variablescode
: execute the argument as a chunk of Python code. The other directives above are available as Python calls with the names above. In addition, the following functions are available inside Pythoncode
only:fail
: mark the test as having failed, but continue executingabort
: mark the test as having failed and stop executingassert_that
: if the condition in the first argument is false, abort the test case
Here is an informative instance of a sample testfile:
test:
suites:
- name: "Language samples test"
setup: # can have yaml and/or code, just as in the cases below
- code:
log('In setup "hi"')
teardown: # can have yaml and/or code, just as in the cases below
- code:
log('In teardown bye')
cases:
- name: "A test defined via yaml directives"
spec:
- log:
- 'Reading from manifest at {language_analyze_sentiment_text:@manifest_source}'
- call:
sample: "language_analyze_sentiment_text"
params:
content:
literal: "happy happy smile hope"
- assert_success: [] # try assert_failure to see how failure looks
- assert_contains:
- message: "Have score and magnitude"
- literal: "score"
- literal: "magnitude"
- assert_contains_any:
- message: "Have magnitude or strength"
- literal: "strength"
- literal: "magnitude"
- assert_contains:
- message: "Score is very positive"
- literal: "score: 0.8"
- assert_contains:
- message: "Magnitude is very positive"
- literal: "magnitude: 0.8"
- assert_excludes:
- message: "Random message"
- literal: "The rain in Spain falls mainly in the plain"
- assert_not_contains: # deprecated: use "assert_excludes" instead
- message: "Random message"
- literal: "The rain in Spain falls mainly in the plain"
# Above is the typical usage
- name: "A test defined via 'code'"
spec:
- code: |
log('Reading from manifest at {language_analyze_sentiment_text:@manifest_source}')
out = call("language_analyze_sentiment_text", content="happy happy smile hope")
assert_success("that should have worked", "well")
assert_contains('score', 'magnitude', message='Have both score and magnitude')
assert_contains_any('strength', 'magnitude', message='Have either strength or magnitude')
import re
score_found = re.search('score: ([0123456789.]+)', out)
assert_that(score_found is not None, 'score matches regexp')
score = float(score_found.group(1))
assert_that(score > 0.7, 'score is high')
magnitude_found = re.search('magnitude: ([0123456789.]+)', out)
assert_that(magnitude_found is not None, 'magnitude matches regexp')
magnitude = float(magnitude_found.group(1))
assert_that(magnitude > 0.7, 'magnitude is high')
assert_excludes("the rain in Spain", message="random message")
# deprecated: use "assert_excludes" instead
assert_not_contains("the rain in Spain",message="random message")
- name: "A test defined via 'code', with explicit calls to specific samples"
spec:
- code: |
_, out = shell("python3 examples/mock-samples/python/language-v1/analyze_sentiment_request_language_sentiment_text.py -content='happy happy smile hope'")
# You can interleave yaml and code!
- assert_success:
- "that should have worked {}"
- well
- code: |
import re
score_found = re.search('score: ([0123456789.]+)', out) # TODO: Can this be negative?
assert_that(score_found is not None, 'score matches regexp')
score = float(score_found.group(1))
assert_that(score > 0.7, 'score is high')
home = env('HOME')
log('home directory: {}'.format(home))
magnitude_found = re.search('magnitude: ([0123456789.]+)', out)
assert_that(magnitude_found is not None, 'magnitude matches regexp')
magnitude = float(magnitude_found.group(1))
assert_that(magnitude > 0.7, 'magnitude is high')
This test plan has three equivalent representations of the same test,
one with canonical artifact paths in the declarative style (using YAML
directives), the second with canonical artifact paths in the
imperative style (using a code
block), and the third using
absolute artifact paths in the imperative style (which you would
rarely use, since th point of this tool is to not have to hardcode
different paths to semantically identical samples).
Unless you specify explicit paths to each sample (which means your
test plan cannot run for different languages/environments
simultaneously), you will need one or more manifest files
(*.manifest.yaml
) listing the path and identifiers for each sample
in each language/environment. . Refer to the
Manifest file format page for an explanation of
the structure of the *.manifest.yaml
files.
Manifest file format¶
A manifest contains one or more YAML documents that associate each
artifact (sample) of interest on disk with a series of metadata
tags. The YAML documents within the file are separated by the usual
YAML start-document indicator, ---
.
A manifest YAML document has the general structure:
---
type: manifest/XXX
schema_version: 3
XXX:
- item1foo: value
item1bar: value
- The “manifest” in the
type
field defines this YAML document as a manifest. Other document types are silently ignored (this permits putting disparate YAML documents in the same file if desired). - The arbitrary value “XXX” in the
type
field defines the top-level YAML fieldXXX
as containing the actual manifest. - The
schema_version
field is required. - Each item in the
XXX
list is simply a dictionary of tag keys and values. The tag keys that define the metadata used by sample-tester are described below. - Other top-level tags (outside of the
XXX
list) are ignored. They can thus be used for additional metadata not used by sample-tester, and/or for defining YAML anchors in order to reduce duplication in the manifest document.
Tag values can include references to other tags: the value of tag “A”
can reference the value of tag “B” by enclosing the name of tag “B” in
curly brackets: {TAG_B_NAME}
. For example:
name: Zoe
greeting: "Hello, {name}!
will define the same sets of tags as
name: Zoe
greeting: "Hello, Zoe!"
While tags can be referenced arbitrarily deep, no reference can form a loop (ie a tag directly or indirectly including itself).
Here’s a generic manifest file illustrating these features:
---
type: manifest/samples
schema_version: 3
samples:
- environment: python
bin: python3
path: "/home/nobody/api/samples/trivial/method/sample_alice"
sample: "alice"
canonical: "trivial"
- environment: python
bin: python3
path: "/home/nobody/api/samples/complex/method/usecase_bob"
sample: "robert"
tag: "guide"
# A manifest file can contain any number of manifest documents, each
# preceded by the YAML `---` document separator.
---
# In this second YAML document, we make use of YAML anchors (`&`) and
# references (`*`) as well as the manifest file inclusion semantics
# (`{}`) to illustrate how fields common to multiple elements of the
# list may be factored out to reduce code duplication and increase
# understandability.
type: manifest/samples
schema_version: 3
python: &python
- environment: python
bin: python3 # used to run these items
base_path: "/home/nobody/api/samples/"
samples:
- << *python
path: "{base_path}/trivial/method/sample_alice"
sample: "alice2"
canonical: "trivial"
- << *python
path: "{base_path}complex/method/usecase_bob"
sample: "robert2"
tag: "guide"
Tags for sample-tester¶
You may define an arbitrary set of tags for any and all elements in
your manifest, the only restriction being that no tag name you specify
may begin with @
(because that is how we identify “implicit tags”;
see below). Moreover, some manifest tags are of special interest to
sample-tester:
sample
: The unique ID for the sample.path
: The path to the sample source code on disk.environment
: A label used to group samples that share the same programming language or execution environment. In particular, artifacts with the samesample
but differentenvironment
s are taken to represent the same conceptual sample, but implemented in the different languages/environments; this allows a test specification to refer to thesample
s only and sample-tester will then run that test for each of theenvironment
s available.invocation
: The command line to use to run the sample. The invocation typically makes use of two features for flexibility:- manifest tag inclusion: By including a
{TAG_NAME}
,invocation
(just like any tag) can include the value of another tag. - tester argument substitution: By including a
@args
literal, theinvocation
tag can specify where to insert the sample parameters as determined by the sample-tester from the test plan file.
Thus, the following would be the typical usage for Java, where each sample item in the manifest includes a
class_name
tag and ajar
tag:invocation: "java {jar} -D{class_name} -Dexec.arguments='@args'"
- manifest tag inclusion: By including a
chdir
: The working directory to be in before invoking the sample.(deprecated)
bin
: The executable used to run the sample. The samplepath
and arguments are appended to the value of this tag to form the command line that the tester runs.
The sample-test runner also automatically adds certain implicit
tags to manifest elements when it reads them from YAML
files. Implicit tag names all begin with the symbol @
:
@manifest_source
: The full path, including filename, to the manifest file from which this particular element was read.@manifest_dir
: The directory part of@manifest_source
, without the trailing filename. For example, depending on your particular set-up, you may wish to reference{@manifest_dir}
as part of the value of yourchdir
tag.
Advanced usage: you can tell sample-tester to use different key names than the ones above. For example, to use keys some_name
, how_to_call
, and switch_path
instead of sample
, invocation
, and chdir
, respectively, you would simply specify this flag when calling sample-tester:
-c tag:some_name:how_to_call,switch_path
Here’s a typical manifest file:
---
type: manifest/samples
schema_version: 3
samples:
- environment: java
invocation: "{jar} -D{class} {path} @args"
path: "examples/mock-samples/java/language-v1/AnalyzeSentiment"
class: AnalyzeSentiment
jar: "./do_java"
chdir: "examples/mock-samples/java/"
sample: "language_analyze_sentiment_text"
- environment: python
bin: "python3"
path: "examples/mock-samples/python/language-v1/analyze_sentiment_request_language_sentiment_text.py"
sample: "language_analyze_sentiment_text"
- environment: bash
# notice: no "bin:" because artifacts are already executable
path: "examples/mock-samples/sh/language-v1/analyze_sentiment.sh"
sample: "language_analyze_sentiment_text"
Running tests¶
To run the tests you have defined, do the following:
Prepare your environment. For example, to run tests against Google APIs, ensure you have credentials set up:
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/your/creds.json
Run the tester, specifying your manifest (any number of
*.manifest.yaml
files) and test plan (any number of other*.yaml
files):sample-tester examples/convention-tag/language.test.yaml examples/convention-tag/language.manifest.yaml
Command-line flags¶
Basic usage¶
sampletester TEST.yaml [TEST.yaml ...] [MANIFEST.manifest.yaml ...] [--envs=REGEX] [--suites=REGEX] [--cases=REGEX] [--fail-fast]
where:
- there can be any number of
TEST.yaml
testplan files - there can be any number of
MANIFEST.manifest.yaml
manifest files --envs
,--suites
, and--cases
are Python-style regular expressions (beware shell-escapes!) to select which environments, suites, and cases to run, based on their names. All the environemnts, suites, or cases will be selected to run by default if the corresponding flag is not set. Note that if an environment is not selected, its suites are not selected regardless of--suites
; if a suite is not selected, its testcases are not selected regardless of--cases
.--fail-fast
makes execution stop as soon as a failing test case is encountered, without executing any remaining test cases.
Controlling the output¶
In all cases, sample-tester
exits with a non-zero code if there were any errors in the flags, test config, or test execution.
In addition, by default sampletester
prints the status of test cases to stdout. This output is controlled by the following flags:
--verbosity
(-v
): controls how much output to show for passing tests. The default is a “summary” view, but “quiet” (no output) and “detailed” (full case output) options are available.--suppress_failures
(-f
): Overrides the default behavior of showing output for failing test cases, regardless of the--verbosity
setting--xunit=FILE
outputs a test summary in xUnit format toFILE
(use-
for stdout).
Advanced usage¶
The tester uses a “convention” to match sample names in the testplan
to actual, specific files on disk for given languages and
environments. Each convention may choose to take some set-up
arguments. You can specify an alternate convention and/or convention
arguments via the flag --convention=CONVENTION:ARG,ARGS
. The
default convention is tag:sample
, which uses the
sample
key in the manifest files. To use, say, the target
key in the manifest, simply pass --convention=tag:target
.
If you want to define an additional convention, refer to the
documention in the repo on how to do so. If you do have such an
additional convention defined, you may use the --convention
flag
to select it and give it any desired arguments, as above.