Add unit tests for services layer
36 tests covering build_families (linear chains, branching, disjoint, Shedinja case), resolve_base_form, to_roman (parametrized), and strip_roman_suffix including round-trip verification. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
---
|
||||
# nuzlocke-tracker-iam7
|
||||
title: Unit tests for services layer
|
||||
status: draft
|
||||
status: completed
|
||||
type: task
|
||||
priority: normal
|
||||
created_at: 2026-02-10T09:33:08Z
|
||||
updated_at: 2026-02-10T09:33:08Z
|
||||
updated_at: 2026-02-21T12:01:23Z
|
||||
parent: nuzlocke-tracker-yzpb
|
||||
---
|
||||
|
||||
@@ -12,10 +13,13 @@ Write unit tests for the business logic in `backend/src/app/services/`. Currentl
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Test family resolution with simple linear evolution chains (e.g. A → B → C)
|
||||
- [ ] Test family resolution with branching evolutions (e.g. Eevee)
|
||||
- [ ] Test family resolution with region-specific evolutions
|
||||
- [ ] Test edge cases: single-stage Pokemon, circular references (if possible), missing data
|
||||
- [x] Test family resolution with simple linear evolution chains (e.g. A → B → C)
|
||||
- [x] Test family resolution with branching evolutions (e.g. Eevee / Shedinja)
|
||||
- [x] Test disjoint chains remain separate families
|
||||
- [x] Test edge cases: empty list, single-stage Pokemon, base form, middle form
|
||||
- [x] Test resolve_base_form: linear, branching, Shedinja, not-in-any-evolution
|
||||
- [x] Test to_roman: parametrized 1–100, genlocke sequence I–V
|
||||
- [x] Test strip_roman_suffix: II/III/IV/X, no suffix, round-trip with to_roman
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
174
backend/tests/test_services.py
Normal file
174
backend/tests/test_services.py
Normal file
@@ -0,0 +1,174 @@
|
||||
"""Unit tests for the services layer (families, naming utilities)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.services.families import build_families, resolve_base_form
|
||||
from app.services.naming import strip_roman_suffix, to_roman
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimal Evolution stand-in — only the two fields the services touch
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Evo:
|
||||
"""Lightweight stand-in for app.models.evolution.Evolution."""
|
||||
|
||||
def __init__(self, from_id: int, to_id: int) -> None:
|
||||
self.from_pokemon_id = from_id
|
||||
self.to_pokemon_id = to_id
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# build_families
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBuildFamilies:
|
||||
def test_empty_evolutions_returns_empty_dict(self):
|
||||
assert build_families([]) == {}
|
||||
|
||||
def test_linear_chain(self):
|
||||
# A(1) → B(2) → C(3)
|
||||
evos = [Evo(1, 2), Evo(2, 3)]
|
||||
families = build_families(evos)
|
||||
assert set(families[1]) == {1, 2, 3}
|
||||
assert set(families[2]) == {1, 2, 3}
|
||||
assert set(families[3]) == {1, 2, 3}
|
||||
|
||||
def test_branching_evolutions(self):
|
||||
# Eevee-like: 1 → 2, 1 → 3, 1 → 4
|
||||
evos = [Evo(1, 2), Evo(1, 3), Evo(1, 4)]
|
||||
families = build_families(evos)
|
||||
assert set(families[1]) == {1, 2, 3, 4}
|
||||
assert set(families[2]) == {1, 2, 3, 4}
|
||||
assert set(families[4]) == {1, 2, 3, 4}
|
||||
|
||||
def test_disjoint_chains_are_separate_families(self):
|
||||
# Chain 1→2 and independent chain 3→4
|
||||
evos = [Evo(1, 2), Evo(3, 4)]
|
||||
families = build_families(evos)
|
||||
assert set(families[1]) == {1, 2}
|
||||
assert set(families[3]) == {3, 4}
|
||||
assert 3 not in set(families[1])
|
||||
assert 1 not in set(families[3])
|
||||
|
||||
def test_shedinja_case(self):
|
||||
# Nincada(1) → Ninjask(2) and Nincada(1) → Shedinja(3)
|
||||
evos = [Evo(1, 2), Evo(1, 3)]
|
||||
families = build_families(evos)
|
||||
assert set(families[1]) == {1, 2, 3}
|
||||
assert set(families[3]) == {1, 2, 3}
|
||||
|
||||
def test_pokemon_not_in_any_evolution_not_in_result(self):
|
||||
evos = [Evo(1, 2)]
|
||||
families = build_families(evos)
|
||||
assert 99 not in families
|
||||
|
||||
def test_all_family_members_have_identical_family_list(self):
|
||||
evos = [Evo(10, 11), Evo(11, 12)]
|
||||
families = build_families(evos)
|
||||
assert set(families[10]) == set(families[11]) == set(families[12])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# resolve_base_form
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestResolveBaseForm:
|
||||
def test_pokemon_not_in_any_evolution_returns_itself(self):
|
||||
assert resolve_base_form(99, []) == 99
|
||||
|
||||
def test_base_form_returns_itself(self):
|
||||
# A(1) → B(2): base of 1 is still 1
|
||||
evos = [Evo(1, 2)]
|
||||
assert resolve_base_form(1, evos) == 1
|
||||
|
||||
def test_final_form_returns_base(self):
|
||||
# A(1) → B(2) → C(3): base of 3 is 1
|
||||
evos = [Evo(1, 2), Evo(2, 3)]
|
||||
assert resolve_base_form(3, evos) == 1
|
||||
|
||||
def test_middle_form_returns_base(self):
|
||||
# A(1) → B(2) → C(3): base of 2 is 1
|
||||
evos = [Evo(1, 2), Evo(2, 3)]
|
||||
assert resolve_base_form(2, evos) == 1
|
||||
|
||||
def test_branching_evolution_base(self):
|
||||
# 1 → 2, 1 → 3: base of both 2 and 3 is 1
|
||||
evos = [Evo(1, 2), Evo(1, 3)]
|
||||
assert resolve_base_form(2, evos) == 1
|
||||
assert resolve_base_form(3, evos) == 1
|
||||
|
||||
def test_shedinja_resolves_to_nincada(self):
|
||||
# Nincada(1) → Ninjask(2), Nincada(1) → Shedinja(3)
|
||||
evos = [Evo(1, 2), Evo(1, 3)]
|
||||
assert resolve_base_form(3, evos) == 1
|
||||
|
||||
def test_empty_evolutions_returns_self(self):
|
||||
assert resolve_base_form(42, []) == 42
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# to_roman
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestToRoman:
|
||||
@pytest.mark.parametrize(
|
||||
"n, expected",
|
||||
[
|
||||
(1, "I"),
|
||||
(2, "II"),
|
||||
(3, "III"),
|
||||
(4, "IV"),
|
||||
(5, "V"),
|
||||
(6, "VI"),
|
||||
(9, "IX"),
|
||||
(10, "X"),
|
||||
(11, "XI"),
|
||||
(14, "XIV"),
|
||||
(40, "XL"),
|
||||
(50, "L"),
|
||||
(90, "XC"),
|
||||
(100, "C"),
|
||||
],
|
||||
)
|
||||
def test_converts_integer_to_roman(self, n: int, expected: str):
|
||||
assert to_roman(n) == expected
|
||||
|
||||
def test_typical_genlocke_sequence(self):
|
||||
# Lineage names: Heracles I, II, III, IV, V
|
||||
assert [to_roman(i) for i in range(1, 6)] == ["I", "II", "III", "IV", "V"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# strip_roman_suffix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestStripRomanSuffix:
|
||||
def test_strips_roman_numeral_ii(self):
|
||||
assert strip_roman_suffix("Heracles II") == "Heracles"
|
||||
|
||||
def test_strips_roman_numeral_iii(self):
|
||||
assert strip_roman_suffix("Athena III") == "Athena"
|
||||
|
||||
def test_strips_roman_numeral_iv(self):
|
||||
assert strip_roman_suffix("Nova IV") == "Nova"
|
||||
|
||||
def test_strips_roman_numeral_x(self):
|
||||
assert strip_roman_suffix("Zeus X") == "Zeus"
|
||||
|
||||
def test_no_suffix_returns_unchanged(self):
|
||||
assert strip_roman_suffix("Apollo") == "Apollo"
|
||||
|
||||
def test_name_with_i_suffix(self):
|
||||
# Single "I" at end is a valid roman numeral suffix
|
||||
assert strip_roman_suffix("Heracles I") == "Heracles"
|
||||
|
||||
def test_round_trip_with_to_roman(self):
|
||||
base = "Heracles"
|
||||
for n in range(1, 6):
|
||||
suffixed = f"{base} {to_roman(n)}"
|
||||
assert strip_roman_suffix(suffixed) == base
|
||||
Reference in New Issue
Block a user