| name | bio-applied-testing-cicd |
| description | Write pytest tests/fixtures for bio functions and GitHub Actions CI with pytest-cov, ruff, black, mypy. Use when adding tests to a bio tool, writing conftest.py fixtures, or building tests.yml/lint.yml CI workflows. |
| tool_type | python |
| primary_tool | pytest |
Testing and CI/CD for Bioinformatics
When to Use
- Adding unit tests to a bioinformatics module (sequence parsing, coordinate math, scoring functions)
- Writing
conftest.py fixtures for temp FASTA/VCF files or shared test data
- Setting up GitHub Actions to run
pytest + coverage on every push/PR
- Adding lint/type-check CI (ruff, black, mypy) alongside tests
- Deciding what edge cases to test for biological data (0-based vs 1-based coords, IUPAC ambiguity codes, strand)
Version Compatibility
pytest ≥7.4, pytest-cov ≥4.1, Python ≥3.10, GitHub Actions actions/checkout@v4, actions/setup-python@v5, codecov/codecov-action@v4.
Prerequisites
pip install pytest pytest-cov ruff black isort mypy
Assumes a package layout with src/<pkg>/ and tests/ (see Project Structure below), and basic familiarity with Python functions/classes.
Goal: Test bioinformatics functions correctly, including the biology-specific edge cases that plain "does it run" tests miss.
Approach: Write the module, then a TestClass per function plus @pytest.mark.parametrize for the same assertion across many sequences.
def gc_content(sequence: str) -> float:
"""Calculate GC content as a percentage (0-100). Empty input -> 0.0."""
if not sequence:
return 0.0
seq = sequence.upper()
gc = seq.count('G') + seq.count('C')
return (gc / len(seq)) * 100
def reverse_complement(sequence: str) -> str:
"""Return the reverse complement of a DNA sequence; unknown bases -> 'N'."""
complement = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G', 'N': 'N',
'a': 't', 't': 'a', 'g': 'c', 'c': 'g', 'n': 'n'}
return ''.join(complement.get(base, 'N') for base in reversed(sequence))
def find_motif(sequence: str, motif: str) -> :
positions, start = [],
:
pos = sequence.find(motif, start)
pos == -:
positions.append(pos + )
start = pos +
positions
import pytest
from bio_utils import gc_content, reverse_complement, find_motif
class TestGCContent:
def test_balanced(self):
assert gc_content("ATGC") == 50.0
def test_empty_sequence(self):
"""Empty sequence must return 0.0, not raise."""
assert gc_content("") == 0.0
def test_lowercase(self):
assert gc_content("atgc") == 50.0
def test_with_n_bases(self):
"""N counts toward length but not toward GC."""
assert gc_content("GCNN") == 50.0
class TestReverseComplement:
def test_palindromic_site(self):
"""EcoRI site GAATTC is its own reverse complement."""
assert reverse_complement("GAATTC") == "GAATTC"
def test_handles_n(self):
assert reverse_complement("ATNG") == "CNAT"
class :
():
find_motif(, ) == [, , ]
():
find_motif(, ) == []
():
gc_content(seq) == expected
Goal: Reuse temp files and biological test data across many tests without duplicating setup.
Approach: Put shared fixtures in conftest.py; use the built-in tmp_path fixture for real files on disk.
import pytest
@pytest.fixture
def sample_fasta_content():
"""Two-record FASTA string for parser tests."""
return (
">gene1 beta-globin\n"
"ATGGTGCACCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAAC\n"
">gene2 alpha-globin\n"
"ATGGTGCTGTCTCCTGCCGACAAGACCAACGTCAAGGCCGCCTGGGGTAAGGTCGGCGCG\n"
)
@pytest.fixture
def sample_fasta_file(tmp_path, sample_fasta_content):
"""Write sample_fasta_content to a real temp file and return its path."""
fasta_path = tmp_path / "test.fasta"
fasta_path.write_text(sample_fasta_content)
return fasta_path
def test_parse_fasta(sample_fasta_content):
from io import StringIO
from Bio import SeqIO
records = list(SeqIO.parse(StringIO(sample_fasta_content), "fasta"))
assert len(records) == 2
assert records[0].id == "gene1"
def test_fasta_file_exists(sample_fasta_file):
assert sample_fasta_file.exists()
assert sample_fasta_file.suffix == ".fasta"
pytest Commands
pytest -v
pytest test_bio_utils.py::TestGCContent
pytest -x
pytest -k "gc or reverse"
pytest --cov=bio_utils --cov-report=html
GitHub Actions CI
name: Tests
on:
push: { branches: [main] }
pull_request: { branches: [main] }
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "${{ matrix.python-version }}" }
- run: pip install pytest pytest-cov && pip install -r requirements.txt
- run: pytest --cov=src --cov-report=xml --cov-report=term-missing
- uses: codecov/codecov-action@v4
with: { file: coverage.xml, fail_ci_if_error: }
name: Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install ruff black isort mypy
- run: black --check src/ tests/
- run: ruff check src/ tests/
- run: mypy src/ --ignore-missing-imports
Project Structure
my_tool/
├── .github/workflows/{tests.yml, lint.yml}
├── src/my_tool/{__init__.py, io.py, analysis.py, utils.py}
├── tests/{conftest.py, test_io.py, test_analysis.py, data/}
├── pyproject.toml
└── requirements.txt
Bioinformatics Testing Checklist
| Category | What to test |
|---|
| Edge cases | Empty, single-base, very long sequences |
| Case handling | Lowercase, uppercase, mixed |
| Ambiguous bases | N, R, Y, other IUPAC codes |
| Coordinates | 0-based vs 1-based, inclusive vs exclusive |
| Strand | Forward, reverse, reverse complement |
| File formats | Malformed, empty, compressed |
| Numeric | Float comparisons with tolerance (pytest.approx) |
Pitfalls
- Coordinate systems: BED is 0-based half-open; VCF/GFF is 1-based inclusive — mixing them causes off-by-one variant/interval errors
- Float comparison: never use
== on p-values or scores; use pytest.approx()
- Test data size: commit small synthetic files to
tests/data/, never full genomes/BAMs
- Fixture scope: default fixture scope is per-function; use
scope="module" only for expensive, read-only setup to avoid state leaking between tests
- CI matrix drift: pin the same Python versions in
tests.yml that you claim to support in pyproject.toml
See Also
- bio-workflow-management-snakemake-workflows
- bio-workflow-management-nextflow-pipelines
- bio-reporting-automated-qc-reports
- bio-sequence-io-read-sequences