- CLAUDE.md with architecture overview, conventions, and strict commit rules - docs/architecture-registries.md: registry pattern details - docs/color-spaces.md: color space system guide - docs/testing.md: BDD testing guide with mamba - docs/nodes-comfyui.md: ComfyUI nodes reference - docs/data-models.md: PixelColor and PixelPalette models
7.6 KiB
Guide des tests BDD avec Mamba
Pourquoi Mamba
Le projet utilise Mamba car l'auteur vient du monde Ruby/RSpec. Mamba est le framework Python le plus proche de RSpec avec sa syntaxe description/context/it.
- Mamba : https://github.com/nestorsalceda/mamba (framework BDD)
- Expects : bibliothèque de matchers (équivalent de
shoulden RSpec)
Commandes
# Commande standard (OBLIGATOIRE avant tout PR)
mamba spec/ --enable-coverage --format=documentation
# Tests rapides pendant le dev
mamba spec/ --format=documentation
# Un seul fichier spec
mamba spec/pixel_palette_spec.py --enable-coverage
# Un seul dossier de specs
mamba spec/color/ --format=documentation
Structure des specs
Les specs sont organisées en miroir de lib/ :
spec/
├── conftest.py # Configuration commune
├── pixel_color_spec.py # -> lib/pixel_color.py
├── pixel_palette_spec.py # -> lib/pixel_palette.py
└── color/
├── color_space_registry_spec.py # -> lib/color/color_space_registry.py
├── color_space_behavior_spec.py # -> lib/color/color_space_context.py
├── rgb_exporter_spec.py # -> lib/color/color_spaces/rgb/rgb_exporter.py
└── color_spaces/
├── rgb/
│ ├── rgb_mixer_spec.py # -> lib/color/color_spaces/rgb/rgb_mixer.py
│ └── rgb_exporter_spec.py # -> lib/color/color_spaces/rgb/rgb_exporter.py
└── hsv/
└── hsv_mixer_spec.py # -> lib/color/color_spaces/hsv/hsv_mixer.py
Règle : spec_pending/ contient les specs désactivées (en cours de travail).
Anatomie d'une spec
# spec/pixel_color_spec.py
from spec.conftest import * # Config commune
from mamba import description, context, it, before
from expects import expect, equal, be_true # Matchers
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from lib.pixel_color import PixelColor # Import du code testé
with description('PixelColor') as self: # describe (le sujet)
with context('Création et initialisation'): # context (la situation)
with it('crée une couleur noir par défaut'): # it (le comportement attendu)
color = PixelColor(0, 0, 0)
expect(color.r).to(equal(0))
with context('#to_hex'): # convention : #method pour les methodes
with it('renvoie la couleur au format hexadécimal'):
color = PixelColor(255, 128, 64)
expect(color.to_hex()).to(equal("#FF8040"))
Équivalences RSpec -> Mamba
| RSpec | Mamba | Rôle |
|---|---|---|
describe |
description |
Bloc principal (sujet testé) |
context |
context |
Bloc de contexte (situation) |
it |
it |
Cas de test individuel |
before(:each) |
before.each |
Setup avant chaque test |
expect(x).to eq(y) |
expect(x).to(equal(y)) |
Assertion |
let(:color) |
variable dans before.each |
Lazy setup |
Matchers expects courants
from expects import (
expect, equal, be_true, be_false, be_none,
be_empty, have_length, contain,
start_with, end_with,
be_above, be_below, be_above_or_equal, be_below_or_equal,
raise_error
)
# Égalité
expect(color.r).to(equal(255))
# Booléens
expect(palette.is_empty).to(be_true)
# Collections
expect(registry.available_spaces()).to(contain('rgb'))
expect(palette.colors).to(have_length(5))
expect(colors).to(be_empty)
# Chaines
expect(formatted).to(start_with("#"))
expect(formatted).to(end_with(";"))
# Erreurs
expect(lambda: registry.get_mixer_class('unknown')).to(
raise_error(ValueError, 'Espace colorimétrique inconnu: unknown')
)
# Comparaisons
expect(color.r).to(be_above_or_equal(0))
expect(color.r).to(be_below_or_equal(255))
Setup et teardown
with description('ColorSpaceRegistry'):
# Appelé avant chaque `it` dans tout le bloc description
def before_each(self):
ColorSpaceRegistry._spaces = {} # Reset du registry
with context('when registering'):
with it('registers a space'):
ColorSpaceRegistry.register('rgb', mock_exporter)
expect(ColorSpaceRegistry.available_spaces()).to(contain('rgb'))
Important : les registries sont des singletons (variables de classe). Il faut les reset dans before_each pour éviter les effets de bord entre tests.
Mocks
from unittest.mock import Mock
mock_exporter = Mock()
mock_mixer = Mock()
ColorSpaceRegistry.register('rgb', mock_exporter, mock_mixer)
result = ColorSpaceRegistry.get_mixer_class('rgb')
expect(result).to(equal(mock_mixer))
Pour tester les exporters en isolation, un MockPixelColor existe :
class MockPixelColor:
def __init__(self, r, g, b, name=""):
self.r = r
self.g = g
self.b = b
self.name = name
Données de test
Le fichier mamba_config.py fournit des palettes de test :
from mamba_config import PALETTES_SAMPLES
SAMPLE_GIMP_PALETTE # Format GIMP .gpl complet
SAMPLE_HEX_PALETTE # Couleurs en hexadécimal
SAMPLE_RGB_PALETTE # Couleurs en rgb()
MALFORMED_PALETTE # Données invalides (tests d'erreur)
Coverage
Configuration dans .coveragerc :
[run]
source = .
omit =
*/venv/*
*/tests/*
*/spec/* # Les specs elles-memes
*/nodes/* # Les nodes ComfyUI (non testés)
__init__.py
nodes.py
mamba_config.py
Philosophie : on mesure la couverture de lib/ uniquement. Les nodes ComfyUI sont des wrappers et ne sont pas testés unitairement.
Rapports HTML dans htmlcov/.
Ce qui est testé vs ce qui ne l'est pas
Testé (lib/)
- PixelColor : création, copie, hex, distance
- PixelPalette : gradient, monochrome, manipulation
- ColorSpaceRegistry : enregistrement, recherche, fusion, erreurs
- RGBMixer : mélange linéaire
- HSVMixer : mélange HSV
- RGBExporter : tous les formats de sortie (hex, rgb, hsl, css, gimp...)
Non testé (volontairement)
- nodes/ : wrappers ComfyUI (tests d'intégration uniquement si nécessaire)
- __init__.py : registration ComfyUI
Lacunes connues
- Parsers (gimp_parser, generic_parser, adobe_aco_parser) : pas de specs
- Palette exporters (gimp_exporter, hex_exporter, rgb_exporter au niveau palette) : pas de specs
spec_pending/contient des tests désactivés a reprendre
Écrire une nouvelle spec
Template pour un nouveau composant :
# spec/parsers/gimp_parser_spec.py
from spec.conftest import *
from mamba import description, context, it, before
from expects import expect, equal, have_length, be_true, be_false
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from lib.parsers.gimp_parser import GimpParser
with description('GimpParser') as self:
with before.each:
self.parser = GimpParser()
with context('#can_parse'):
with it('returns True for valid GIMP palette content'):
content = "GIMP Palette\nName: Test\n#\n255 0 0\tRouge"
expect(self.parser.can_parse(content)).to(be_true)
with it('returns False for non-GIMP content'):
content = "#FF0000\n#00FF00"
expect(self.parser.can_parse(content)).to(be_false)
with context('#parse'):
with it('extracts colors from GIMP palette'):
content = "GIMP Palette\nName: Test\n#\n255 0 0\tRouge"
colors = self.parser.parse(content)
expect(colors).to(have_length(1))
expect(colors[0].r).to(equal(255))