Testing an annotator
This page tests the FollowupAnnotator of Writing an annotator,
the class from “A minimal annotator” on that page. Every example below is
part of a pytest module that runs in GAIn’s own test suite, and it uses
the declared API only. The test module defines FollowupAnnotator and
build_followup_annotator exactly as that page shows them.
Registering in a test
A pipeline resolves an annotator’s type through the annotator registry. An
installed package puts its type there through an entry point
(Annotator plugins). A test runs the pipeline in its own process, so it does
not need an installed entry point:
register_annotator_factory()
adds the annotator factory to the registry of that process. Call it at
import time of the test module, so the type is registered before the
first test loads a pipeline:
register_annotator_factory("followup", build_followup_annotator)
The “Discovering” section of Annotator plugins says why this is not a substitute for the entry point in a package meant to be installed.
A unit test with a seeded context
The followup annotator reads only the context, so its unit test needs
no genomic resource. The pipeline loader still takes a repository: an
empty directory repository in tmp_path is enough. The test loads a
one-annotator pipeline and passes the context that the earlier annotators
would have produced as the second argument of
annotate():
def test_followup_answers_from_the_context(tmp_path: pathlib.Path) -> None:
"""The annotator answers from the context that the test seeds."""
repo = grr.build_genomic_resource_repository({
"id": "empty",
"type": "directory",
"directory": str(tmp_path / "grr"),
})
pipeline = load_pipeline_from_yaml(
"- followup: {}", repo, work_dir=tmp_path / "work")
with pipeline.open() as pipeline:
allele = VCFAllele("chr1", 10, "A", "T")
selected = pipeline.annotate(
allele,
{"phyloP7way": 1.5, "clinical_significance": "Pathogenic"})
not_selected = pipeline.annotate(allele, {})
no_annotatable = pipeline.annotate(None, {})
assert selected == {
"phyloP7way": 1.5,
"clinical_significance": "Pathogenic",
"followup": "yes",
}
assert not_selected == {"followup": "no"}
assert no_annotatable == {"followup": None}
The pipeline answers the context with the annotator’s attributes merged
into it. With the seeded context, the first answer is
{'phyloP7way': 1.5, 'clinical_significance': 'Pathogenic', 'followup': 'yes'}.
An empty context gives {'followup': 'no'}, and a None annotatable
gives {'followup': None}.
An integration test over a resource
An integration test runs the annotator after the annotator that produces
its input. Here a position_score_annotator produces phyloP7way
from a position score that the test writes into tmp_path. The score
table is a plain tab-separated file: format: tsv needs no bgzip and no
tabix index. Position scores describes the keys of the
genomic_resource.yaml.
def write_position_score(directory: pathlib.Path) -> None:
"""Write a ``phyloP7way`` position score as a plain TSV resource."""
directory.mkdir(parents=True)
(directory / "genomic_resource.yaml").write_text(textwrap.dedent("""
type: position_score
table:
filename: phyloP7way.tsv
format: tsv
chrom:
name: chrom
pos_begin:
name: pos_begin
pos_end:
name: pos_end
scores:
- id: phyloP7way
type: float
name: phyloP7way
"""))
(directory / "phyloP7way.tsv").write_text(
"chrom\tpos_begin\tpos_end\tphyloP7way\n"
"chr1\t10\t10\t1.5\n"
"chr1\t11\t11\t-0.5\n")
The repository is the directory that holds the resource directory, so the resource id is the name of that directory. The test loads a two-entry pipeline and asserts that the plugin read what the upstream annotator produced:
def test_followup_reads_the_upstream_score(tmp_path: pathlib.Path) -> None:
"""The annotator reads the score that the annotator before it wrote."""
write_position_score(tmp_path / "grr" / "phyloP7way")
repo = grr.build_genomic_resource_repository({
"id": "local",
"type": "directory",
"directory": str(tmp_path / "grr"),
})
pipeline = load_pipeline_from_yaml(textwrap.dedent("""
- position_score_annotator:
resource_id: phyloP7way
- followup: {}
"""), repo, work_dir=tmp_path / "work")
with pipeline.open() as pipeline:
conserved = pipeline.annotate(
VCFAllele("chr1", 10, "A", "T"),
{"clinical_significance": "Pathogenic"})
not_conserved = pipeline.annotate(
VCFAllele("chr1", 11, "C", "G"),
{"clinical_significance": "Pathogenic"})
assert conserved["phyloP7way"] == 1.5
assert conserved["followup"] == "yes"
assert not_conserved["phyloP7way"] == -0.5
assert not_conserved["followup"] == "no"
What to assert
One assertion for each attribute that the annotator declares in
get_attribute_specs().The answer for a
Noneannotatable: the base answersNonefor each attribute.used_context_attributes, compared with the context keys that the annotator reads. A reannotation builds its dependency graph from this tuple, and a plain annotation run does not notice a missing name.
Another test of the module checks the declared attributes and the declared reads of the built annotator:
def test_followup_declares_its_context_reads(tmp_path: pathlib.Path) -> None:
"""The annotator declares one attribute and two context reads."""
repo = grr.build_genomic_resource_repository({
"id": "empty",
"type": "directory",
"directory": str(tmp_path / "grr"),
})
pipeline = load_pipeline_from_yaml(
"- followup: {}", repo, work_dir=tmp_path / "work")
annotator = pipeline.annotators[0]
assert [attr.name for attr in annotator.attributes] == ["followup"]
assert annotator.used_context_attributes == (
"phyloP7way", "clinical_significance")
Unsupported fixtures
gain.genomic_resources.testing holds the fixture builders that GAIn’s
own tests use: they are outside the declared API and can change without
notice.