Files
gitea-ci-library/docs/test-plan/tdd-guide.md
T
2026-06-08 09:34:28 +03:00

13 KiB

Test-Driven Development Guide — Gitea Actions CI -kirjasto

Updated: 2026-06-08

Tämä dokumentti kuvaa testivetoisen kehityksen menetelmän, testiarkkitehtuurin ja kehitysloopin tässä projektissa.


Kolmikerroksinen testiarkkitehtuuri

┌──────────────────────────────────────────────────────────────────────┐
│  LAYER 1: Cucumber acceptance / E2E                                  │
│  Cucumber + Gherkin  |  tests/features/<NNNN>-<name>.feature         │
│  Yksi feature file per tiketti. Käyttäjän näkökulma.                 │
│  Tägätty: @mock, @real, @ticket-NNNN                                 │
│  Tiketti on valmis kun @mock-skenaariot GREEN                        │
├──────────────────────────────────────────────────────────────────────┤
│  LAYER 2: Workflow-validointi                                        │
│  Bats  |  tests/workflows.bats                                       │
│  YAML-skeema, input/output -kontrakti                                │
│  Käytetään kun tiketti koskee reusable workflow'ta                   │
├──────────────────────────────────────────────────────────────────────┤
│  LAYER 3: Bash-yksikkötestit                                         │
│  Bats  |  tests/<script>.bats                                        │
│  Red-Green-Refactor per skripti                                      │
│  Käytetään kun tiketti koskee bash-skriptiä                          │
└──────────────────────────────────────────────────────────────────────┘

Periaatteet

  1. Feature file on kova portti, kun tiketti sellaisen vaatii. Tiketti itse kertoo, liittyykö siihen feature file. Jos liittyy ja tiedosto puuttuu → virhe, ei jatketa. Kaikki tiketit eivät tuo käyttäjälle näkyviä ominaisuuksia — teknisille tiketeille (siivous, validointi, konfiguraatio) feature fileä ei vaadita.

  2. Feature file on PO-roolin omistama. Toteuttaja ei kirjoita omaa feature fileään — se olisi itsensä valvomista. Feature file kirjoitetaan PO-toimesta ennen toteutusta. Ainoa sallittu muutos toteutusvaiheessa: @wip-tagin poistaminen skenaariolta, kun se on toteutettu ja menee vihreäksi.

  3. Cucumber first. Jokaisessa tiketissä Gherkin-skenaario(t) kirjoitetaan ennen toteutusta. Skenaario ajetaan mock-kerrosta vasten → Red. Toteutuksen jälkeen → Green.

  4. TDD sisemmällä tasolla. Cucumber-skenaarion alla: Bats-yksikkötesti → Red → toteutus → Green → refactor. Molemmat tasot vihreitä = tiketti valmis.

  5. Tägätty ympäristösopivuus. Skenaariot tägätään sen mukaan, missä ympäristöissä ne voidaan ajaa. Kaksi ympäristöä: paikallinen mock-kerros (@mock) ja oikea testiympäristö (@real). Useimmat skenaariot ajetaan molemmissa. Osa skenaarioista toimii vain toisessa — esimerkiksi OIDC-kirjautuminen vaatii oikean Traefikin (@real), kun taas mock-spesifi edge case toimii vain paikallisesti (@mock).

Tagi Ympäristö Kuvaus
@mock Paikallinen mock tests/helpers/mock-api.sh — Gitea + MinIO mockit
@real Oikea testiympäristö Todellinen Gitea-instanssi + MinIO-klusteri
@ticket-NNNN Skenaariotason tagi. Kertoo minkä tiketin osana skenaario luotiin. Mahdollistaa konfliktinratkaisun: kun vanha skenaario hajoaa, tiedetään mihin tikettiin se kuuluu.
@wip Work in progress — PO:n kirjoittama skenaario jota ei ole vielä toteutettu. CI skipaa (--tags ~@wip). Toteuttaja poistaa tämän tagin kun skenaario menee vihreäksi.
  1. Feature filet scopen mukaan. Käyttäjätoiminnallisuustiketti aloittaa oman feature fileen. Myöhemmät tiketit voivat lisätä skenaarioita samaan tiedostoon, jos scope on sama — skenaariot niputetaan aihealueen, ei tiketin mukaan. Jokainen skenaario tägätään @ticket-NNNN:lla, joten omistajuus säilyy vaikka tiedostossa on usean tiketin skenaarioita. Tekniset tiketit (skriptit, siivous, validointi) eivät tarvitse feature fileä — ne käyttävät Bats-testejä.

  2. Dogfood. Kirjaston oma .gitea/workflows/ci.yml käyttää kirjaston omia reusable workflow'ita. Jokainen push testaa kirjaston itse itsellään.

Kehityslooppi

Kun tiketillä on feature file:

for each scenario in feature file:

  1. Valitse skenaario jossa `@wip`-tagi
     → cucumber <feature> --tags @ticket-NNNN and @wip
     → RED

  2. TDD-sykli
     a. Bats-yksikkötesti → RED
     b. Toteuta minimi → Bats GREEN
     c. Cucumber → GREEN
     d. Poista `@wip`-tagi skenaariolta
     e. Refactor → Bats + Cucumber pysyvät GREEN

  3. Seuraava `@wip`-skenaario

until no @wip scenarios remain

Kun tiketillä ei ole feature fileä (tekninen tiketti), käytetään pelkkää Bats-TDD-sykliä:

1. Bats-yksikkötesti → RED
2. Toteuta minimi → Bats GREEN
3. Refactor → Bats GREEN

DoD (Definition of Done)

Tiketille jolla on feature file:

- [ ] Cucumber: @ticket-NNNN and @mock → kaikki skenaariot GREEN
- [ ] Bats: tiketin .bats-testit GREEN
- [ ] Toteutus: skripti / workflow olemassa
- [ ] Refactor: ei duplikaatiota, virheenkäsittely kunnossa

Tekniselle tiketille (ei feature fileä):

- [ ] Bats: tiketin .bats-testit GREEN
- [ ] Toteutus: skripti / workflow olemassa
- [ ] Refactor: ei duplikaatiota, virheenkäsittely kunnossa

Step definitionit ja mock-kerros

Cucumber step definition (*.steps.sh)
        │
        ├── asettaa ympäristön: source tests/helpers/mock-api.sh
        │       GITEA_API_URL, MINIO_BASE_URL, tokenit, GITHUB_*
        │
        ├── kutsuu skriptiä: bash scripts/<script>.sh <args>
        │
        └── tarkistaa tuloksen: exit code, stdout, mock-lokit

Mock (tests/helpers/mock-api.sh) on jaettu Bats- ja Cucumber-testien välillä. Step definitionit kutsuvat skriptejä suoraan — ei HTTP-kutsuja cucumberista ulospäin.

Mock-infrastruktuuri

Mock on puhdas bash — ei Dockeria, ei WireMockia.

Mock Portti Rajapinta
Gitea REST API 18080 /api/v1/repos/*/statuses/*, /dispatches, /runs, /tags, /commits/*
MinIO S3 API 19000 mc cp, mc ls, mc rm

Esivaatimukset

Työkalu Versio Asennus
Bats 1.11+ brew install bats
Cucumber latest npm install -g @cucumber/cucumber
jq 1.6+ brew install jq
yq 4+ brew install yq
mc (MinIO client) latest brew install minio/stable/mc

Tiedostorakenne

tests/
├── features/
│   ├── <NNNN>-<name>.feature       # Yksi per tiketti
│   └── step_definitions/
│       ├── common.steps.sh          # Jaetut (Given API, Given MinIO…)
│       └── <NNNN>-<name>.steps.sh   # Yksi per tiketti
├── helpers/
│   └── mock-api.sh                  # Jaettu mock-palvelin
├── <script>.bats                    # Yksikkötestit per skripti
└── workflows.bats                   # Workflow-validointi

Testien ajaminen

# Kehitys: tiketin mock-skenaariot (vain @wip, eli toteuttamattomat)
cucumber tests/features/<NNNN>-<name>.feature --tags @mock and @wip

# Kehitys: kaikki tiketin mock-skenaariot (poislukien @wip)
cucumber tests/features/<NNNN>-<name>.feature --tags @mock and ~@wip

# Tikettikohtainen tagi (poislukien @wip)
cucumber tests/features/ --tags @ticket-NNNN and ~@wip

# Kaikki mock-skenaariot (poislukien @wip)
cucumber tests/features/ --tags @mock and ~@wip

# CI-putki: kaikki valmiit skenaariot
cucumber tests/features/ --tags ~@wip

# Oikea ympäristö (CI-putki)
cucumber tests/features/ --tags @real

# Bats-yksikkötestit
bats tests/<script>.bats

# Kaikki Bats-testit
bats tests/

# Workflow-validointi
bats tests/workflows.bats

Living Documentation

Gherkin-skenaariot ovat ensisijaisesti elävää dokumentaatiota ja vasta toissijaisesti testejä. Dokumentaatio kuvaa järjestelmän käyttäytymisen — testit varmistavat, että dokumentaatio pitää paikkansa.

Konfliktinratkaisu @ticket-NNNN-tagien avulla

Kun uusi tiketti muuttaa järjestelmän käyttäytymistä, vanhat skenaariot voivat mennä rikki:

1. Tiketti 0013 toteutettu, uudet skenaariot @ticket-0013
2. Push → CI ajaa cucumber --tags ~@wip
3. Punaista: vanha @ticket-0001-skenaario feilaa
4. Tiimi näkee ristiriidan: @ticket-0013 vs @ticket-0001
5. Tiimi ratkaisee: kumpi skenaario kuvaa oikeaa käyttäytymistä?
   → Jos uusi on oikein → vanha skenaario päivitetään
   → Jos vanha kuvaa toista tapausta → uutta skenaariota tarkennetaan
6. Konflikti ratkaistu → green → PR → master

@ticket-NNNN mahdollistaa sen, että tiedetään kuka skenaarion omistaa — ilman attribuutiota ristiriitatilanteessa ei voida jäljittää mikä muutos aiheutti ongelman.

Test code = production code

Testikoodiin pätevät samat periaatteet kuin tuotantokoodiin. Huono testiarkkitehtuuri tekee ylläpidosta raskasta — testien on oltava helppoja lukea, muuttaa ja laajentaa.

1. Dependency rule: sisäänpäin

Gherkin-featuret (bisneskieli, STABIILI)
    ↓ riippuu
Step definitionit (käännös bisnes → tekninen)
    ↓ riippuu
Mock / Bats (infrastruktuuri, muuttuu teknologian mukana)

Feature ei koskaan viittaa mockiin, porttiin tai tiedostopolkuun. Step definition kääntää. Sama periaate kuin Entities → Use Cases → Adapters.

2. SLAB: yksi abstraktiotaso per kerros

# ❌ Matala ja korkea sekaisin
Scenario: POST /api/v1/repos/statuses returns 201

# ✅ Vain bisneskieltä
Scenario: Developer sees build status on commit

Gherkin = mitä. Step definition = miten. Ei teknisiä yksityiskohtia feature-tasolla.

3. Single responsibility per scenario

Yksi When, yksi lopputulos. Jos skenaariossa on kaksi When-lausetta → pilko kahdeksi skenaarioksi. Scenario Outline + Examples kun sama logiikka eri datalla.

4. Guard clauses step definitioneissa

Tarkista esiehdot heti, palauta early. Ei syviä if-puita:

# ❌ Deep nesting
if [ -n "$GITEA_TOKEN" ]; then
  if [ -n "$GITEA_API_URL" ]; then
    curl ...
  fi
fi

# ✅ Guard clause
[ -z "$GITEA_TOKEN" ] && echo "Missing GITEA_TOKEN" && exit 1
[ -z "$GITEA_API_URL" ] && echo "Missing GITEA_API_URL" && exit 1
curl ...

5. Ei kuollutta koodia

  • @wip korvaa kommentoidut skenaariot — ei #-rivejä, ei /* TODO */
  • Git muistaa poistetun koodin — älä jätä sitä tiedostoon
  • Step definitioneissa ei kuolleita polkuja tai käyttämättömiä parametreja

6. Tiedostorakenne huutaa tarkoitusta

tests/features/
├── commit-status.feature       ← "tämä on commit-status-järjestelmä"
├── report-browsing.feature     ← ei "http-mock" tai "api-wrapper"
└── step_definitions/
    ├── commit-status.steps.sh
    └── common.steps.sh         ← jaetut, ei "utils" tai "helpers"

Tiedostonimi kertoo mitä testataan, ei miten. Ei kansioita kuten unit/, integration/ — ne kertovat testaustavasta, eivät järjestelmästä.

7. Testit ilman infraa — ja infraa vasten

@mock-tagi varmistaa että testit ajetaan paikallisesti ilman oikeaa Gitea-instanssia. Sisempi testikerros ei koskaan riipu ulkoisista palveluista — sama periaate kuin Entities-testit ilman tietokantaa.

Sama testi toimii molemmissa ympäristöissä:

@mock → nopea paikallinen palaute, ei ulkoisia riippuvuuksia
@real → varmistaa että mock-kerros on oikein

Jos skenaario menee vihreäksi @mock:lla mutta punaiseksi @real:lla, mock on epätarkka — se ei mallinna oikeaa järjestelmää oikein. @real-testit validoivat mockin. Ne sulkevat silmukan: mock mahdollistaa nopean kehityksen, @real varmistaa ettei mock valehtele.

Kontekstikuratoinnin periaate

Jokaisen tiketin suunnittelu, Gherkin-määrittely ja toteutus tapahtuvat omissa sessioissaan. Ennen toteutusta ladataan kontekstiin:

  • docs/test-plan/tdd-guide.md — testiarkkitehtuuri ja TDD-menetelmä
  • Tiketin oma feature file — hyväksymiskriteerit
  • Tarvittavat domain-skillit — tdd, implementation, bash-script tms.

Parempi, kuratoitu konteksti = parempi suoritus. Kontekstia ei haeta "kaikkea" — vain se mitä tämä tiketti tarvitsee.