# Bio Applied Testing Cicd

> 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.

- Skill: `pavel-kravchenko/bio-applied-testing-cicd` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pavel-kravchenko/bio-applied-testing-cicd`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pavel-kravchenko/bio-applied-testing-cicd/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: pavel-kravchenko (https://skillmd.com/u/pavel-kravchenko)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/pavel-kravchenko/bio-applied-testing-cicd

---


# 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

```bash
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.

```python
## bio_utils.py
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) -> list:
    """Find all 1-based, overlap-inclusive start positions of motif in sequence."""
    positions, start = [], 0
    while True:
        pos = sequence.find(motif, start)
        if pos == -1:
            break
        positions.append(pos + 1)  # 1-based
        start = pos + 1
    return positions
```

```python
## test_bio_utils.py
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 TestFindMotif:
    def test_overlapping_occurrences(self):
        """Overlapping matches must all be reported."""
        assert find_motif("AAAA", "AA") == [1, 2, 3]

    def test_not_found(self):
        assert find_motif("ATGC", "GGG") == []


@pytest.mark.parametrize("seq,expected", [
    ("GGGG", 100.0), ("AAAA", 0.0), ("ATGC", 50.0), ("GC", 100.0),
])
def test_gc_content_parametrized(seq, expected):
    assert 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.

```python
## conftest.py
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

```bash
pytest -v                                  # verbose, one line per test
pytest test_bio_utils.py::TestGCContent    # run one class
pytest -x                                  # stop at first failure
pytest -k "gc or reverse"                  # keyword filter
pytest --cov=bio_utils --cov-report=html   # coverage report
```

### GitHub Actions CI

```yaml
## .github/workflows/tests.yml
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: false }
```

```yaml
## .github/workflows/lint.yml
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

```text
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

