Skip to content

Latest commit

 

History

History
528 lines (377 loc) · 18.2 KB

File metadata and controls

528 lines (377 loc) · 18.2 KB

Guide de tests — Le Circographe

Statut : stable Public cible : contributeur Dernière vérification : 2026-05-01 Sources de vérité : spec/, bin/test, bin/test_fast, spec/rails_helper.rb, .rspec.

Vocabulaire : voir ../glossary.md. Les exemples de code utilisent le vocabulaire canonique ContributionFormula / Contribution.

Ce document remplace l'ancien trio docs/TDD_GUIDE.md + docs/TESTING_GUIDE.md + docs/CHANGELOG_TDD_SETUP.md qui s'étaient mis à diverger. Pour la priorisation par zones (Zone 1 / 2 / 3), se référer à ../architecture/models.md.

Sommaire

  1. Philosophie TDD
  2. Setup et outillage
  3. Stratégie de tests (3 types)
  4. Workflow TDD au quotidien
  5. Structure et conventions des tests
  6. Outils et commandes
  7. Couverture (SimpleCov)
  8. Gaps connus (snapshot)
  9. CI/CD
  10. Troubleshooting

1. Philosophie TDD

Cycle Red-Green-Refactor

  1. Red : écrire un test qui échoue.
  2. Green : écrire le minimum de code pour le faire passer.
  3. Refactor : améliorer le code en gardant les tests verts.

Principe fondamental

Tester le comportement métier, PAS l'implémentation.

Mauvais (teste l'implémentation interne) :

it "updates status to active using update!" do
  membership.update!(status: :active)
  expect(membership.status).to eq("active")
end

Bon (teste le comportement attendu) :

it "activates a pending membership when payment succeeds" do
  membership = create(:membership, status: :pending)
  payment = create(:payment, membership: membership, status: :success)

  People::PaymentCreator.new(
    person: payment.person,
    amount_cents: payment.total_cents,
    payment_method: payment.payment_method,
    recorded_by_id: payment.recorded_by_id,
    item_type: "Donation",
    item_id: payment.person_id
  ).call

  expect(membership.reload.status).to eq("active")
end

2. Setup et outillage

Gems clés (dans Gemfile, groupe :test)

  • rspec-rails — framework de tests.
  • factory_bot_rails — fabriques de données.
  • simplecov — rapport de couverture.
  • shoulda-matchers — matchers concis pour validations / associations.

Fichiers de configuration

  • spec/spec_helper.rb — config RSpec + activation SimpleCov, groupes (Models, Controllers, Services, Helpers, Jobs, Mailers).
  • spec/rails_helper.rb — config Rails + Shoulda Matchers.
  • bin/rspec — wrapper RSpec sérialisé pour SQLite via flock.

Seuil de couverture

  • Seuil minimum CI : 12 % (objectif progressif vers 60 % sur le cœur métier, +5 % par itération majeure).

3. Stratégie de tests

3.1 Tests d'invariants métier (immuables)

Règles métier core qui ne changeront jamais.

it "prevents overlapping active memberships" do
  person = create(:person)
  create(:membership, person: person, status: :active,
         started_at: Date.current, ended_at: 1.year.from_now)

  overlapping = build(:membership, person: person, status: :active,
                      started_at: 6.months.from_now, ended_at: 18.months.from_now)

  expect(overlapping).not_to be_valid
end

it "requires price_cents to be greater than 0" do
  formula = build(:contribution_formula, price_cents: 0)
  expect(formula).not_to be_valid
end

Ces tests sont sacrés : si tu changes la logique métier et qu'ils échouent, stop — tu as cassé une règle immuable.

3.2 Tests de contrat (interface stable)

Les APIs publiques exposées doivent rester stables.

describe "Contract: Response format" do
  it "always returns a result object" do
    payment = create(:payment)
    result = People::PaymentCreator.new(
      person: payment.person,
      amount_cents: payment.total_cents,
      payment_method: payment.payment_method,
      recorded_by_id: payment.recorded_by_id,
      item_type: "Donation",
      item_id: payment.person_id
    ).call

    expect(result).to respond_to(:success?)
    expect(result).to respond_to(:failure?)
    expect(result).to respond_to(:errors)
  end
end

3.3 Tests de caractérisation (comportement actuel)

Documentent le comportement actuel, pas l'idéal.

# Comportement ACTUEL : Pack 10 n'expire jamais
it "never expires for pack10 contributions" do
  contribution = create(:contribution, :pack10, expires_at: 1.year.ago)
  expect(contribution.expired?).to be false
end

Si dans 6 mois tu changes cette règle :

  1. Le test échoue.
  2. Tu décides : régression ou nouvelle feature ?
  3. Si nouvelle feature → mettre à jour ../domain/business_logic.md + le test.
  4. Si régression → rejeter le changement.

3.4 Patterns spécifiques (services People::*)

Contrat Result

Tous les services People::* retournent un objet avec success?, errors, message et un payload spécifique :

result = People::MembershipCreator.new(...).call
expect(result).to respond_to(:success?)
expect(result).to respond_to(:errors)
expect(result).to respond_to(:message)
expect(result).to respond_to(:membership)

Instrumentation (ActiveSupport::Notifications)

Les services émettent des événements (payment.created, membership.created, membership.upgraded, contribution.created, etc.) :

captured = []
subscriber = ActiveSupport::Notifications.subscribe("membership.created") do |_n, _s, _f, _i, payload|
  captured << payload
end

People::MembershipCreator.new(person: person, membership_type_id: type.id, recorded_by_id: admin.id).call
ActiveSupport::Notifications.unsubscribe(subscriber)

expect(captured).not_to be_empty
expect(captured.first[:person_id]).to eq(person.id)

Turbo / Hotwire (request specs)

post path, params: params, headers: { "Accept" => "text/vnd.turbo-stream.html" }
expect(response.media_type).to eq("text/vnd.turbo-stream.html")
expect(response.body).to include("turbo-stream")

3.5 Request specs CRUD inline (admin)

Les contrôleurs admin simplifiés se testent comme des flows intégrés :

  • Admin::EventsController — création avec title, mise à jour partielle (via compact_blank), redirections et notices.
  • Admin::MembershipTypesControllercreate / update / destroy et validations modèle.
  • Admin::ContributionFormulasControllerupdate / destroy inline ; create délègue à People::ContributionCreator.

4. Workflow TDD au quotidien

Bug ou edge case détecté

  1. Reproduire le bug dans un test (Red).
  2. Fixer (Green).
  3. Refactor.
  4. Commit.

SQLite et concurrence

Avec SQLite, un seul writer peut modifier tmp/test.sqlite3 à la fois. Deux processus RSpec parallèles provoquent des erreurs de type SQLite3::BusyException: database is locked.

Bon usage :

bin/rspec spec/models/payment_line_spec.rb
bin/test
bin/test_fast
bin/test_watch

Éviter :

bundle exec rspec ...
bundle exec guard

en parallèle d'une autre suite de tests, car cela contourne le lockfile projet.

Nouvelle règle métier

  1. Documenter la règle dans ../domain/business_logic.md.
  2. Écrire le test (Red).
  3. Implémenter (Green).
  4. Refactor.
  5. Commit.

Logique existante qui change

  1. Tests existants échouent (Red).
  2. Décider : bug ou feature ?
  3. Si bug → fixer (Green).
  4. Si feature → mettre à jour tests + doc (Green).
  5. Refactor.
  6. Commit.

5. Structure et conventions des tests

spec/
  models/                # Validations, associations, scopes
  requests/              # Tests d'intégration des contrôleurs
    admin/
    public/
  services/              # Service objects
  helpers/
  factories/
  support/               # Helpers partagés
  rails_helper.rb
  spec_helper.rb

Les controllers/ purs sont dépréciés au profit de requests/.

Conventions de nommage

RSpec.describe User, type: :model do
  describe "validations" do
    it { should validate_presence_of(:email) }
  end

  describe "associations" do
    it { should have_many(:memberships) }
  end

  describe "#some_method" do
    context "when condition" do
      it "returns expected result" do
        # ...
      end
    end
  end
end

6. Outils et commandes

Scripts locaux

  • bin/test — suite complète avec couverture.
  • bin/test_fast — models + services uniquement.
  • bin/test_watch — watch mode (requiert Guard, gem optionnelle).

Workflow lint local (aligné CI)

Avant une PR : pas de refactor métier ni renommage de concepts domaine sans accord — voir le glossaire.

bundle exec rubocop --format simple --force-exclusion
bundle exec rspec
  • CI : le job lint dans .github/workflows/ci-dev.yml exécute bin/rubocop --format github --force-exclusion (mêmes règles, format pour les annotations GitHub).
  • Ciblage : bundle exec rubocop --only NomDuCop chemins… --force-exclusion

Voir les offenses dans ton terminal

  • Synthèse (compte par cop) :

    bundle exec rubocop --format offenses --force-exclusion
  • Liste détaillée fichier/ligne (lisible vite) :

    bundle exec rubocop --format clang --force-exclusion
  • Limiter à un dossier ou un fichier :

    bundle exec rubocop app/helpers --force-exclusion
    bundle exec rubocop app/models/user.rb --force-exclusion
  • Tester un ou plusieurs cops précis (utile avant d’activer quelque chose dans .rubocop.yml) :

    bundle exec rubocop app --only Rails/HelperInstanceVariable --force-exclusion
  • Prévisualiser un lot de cops sans écrire :

    bundle exec rubocop --only NomDuCop chemins... --force-exclusion

    Puis corriger avec bundle exec rubocop -a (safe) ou -A (plus agressif — à utiliser avec prudence).

RSpec

bundle exec rspec
bundle exec rspec --format documentation
bundle exec rspec spec/models/user_spec.rb
bundle exec rspec --tag focus
bundle exec rspec --only-failures
bundle exec rspec --seed 12345

FactoryBot

FactoryBot.define do
  factory :user do
    email { "user@example.com" }
    password { "password123" }
    system_role { :member }
  end
end
user = create(:user)
user = create(:user, email: "custom@example.com")
user = build(:user)

Shoulda Matchers

it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email) }
it { should validate_length_of(:password).is_at_least(8) }

it { should have_many(:memberships) }
it { should belong_to(:person) }

7. Couverture (SimpleCov)

Pourquoi pas 100 % ?

La couverture dit quelles lignes sont exécutées par les tests, pas si elles sont bien testées. 100 % n'égale pas 100 % de confiance.

Bonnes questions :

  • Tous les invariants métier sont-ils couverts ?
  • Les edge cases critiques sont-ils couverts ?
  • Les chemins d'erreur sont-ils testés ?

Générer le rapport

bin/test
# ou ciblé :
bundle exec rspec spec/models/contribution_formula_spec.rb

Rapport : coverage/index.html.

Lire le rapport

  1. Ouvrir coverage/index.html.
  2. Cliquer sur un fichier (ex. app/models/contribution_formula.rb).
  3. Couleurs :
    • Vert : ligne couverte.
    • Rouge : ligne jamais exécutée.
    • Gris : code mort / non exécutable.

Seuils recommandés

  • Minimum CI : 52 % (actuel, suite complète).
  • Acceptable : 30–40 %.
  • Bon : 50–60 %.
  • Excellent : 70 %+ avec qualité.

Focus qualité plutôt que quantité

  • Tous les invariants métier testés.
  • Tous les edge cases critiques testés.
  • Tous les services People::* testés.
  • Tous les contrôleurs Zone 1 testés.

8. Gaps connus (snapshot 2025-01-31, à revalider)

Ces listes sont un instantané et doivent être recroisées avec ../internal/todo.md et ../architecture/models.md avant action.

Models testés

User, Person, Membership, Payment, PaymentLine, MembershipType, Contribution (couverture business complète), Event (basique).

Models non testés

ContributionFormula, AccountClaim, Attendance, AttendanceList, Blog, Tag, TagBlog, PriceCatalog, PriceEntry, PaymentAuditLog, MemberNumberHistory, EventAttendee, Session, UserService.

Contrôleurs testés (Zone 1)

Admin::UsersController, Admin::PaymentsController, Admin::MembershipsController, Admin::EventsController, Admin::DashboardController, SessionsController, RegistrationsController, CheckoutController.

Services testés

L'ensemble des services People::*, AccountClaimManagement::*, AttendanceManagement::*, AttendanceListManagement::*, BlogManagement::*, OpeningHoursManagement::*, NewsletterManagement::*, UserManagement::* (Updater, Deleter), EventManagement::*, MemberNumberManagement::*.

Priorités courtes

  1. OpeningHoursController (mise à jour via cache).
  2. Newsletter (flow authentifié) via NewsletterManagement::NewsletterUpdater.
  3. UserDeleter (suppression / archivage sécurisé).
  4. Observabilité : tests d'événements (ActiveSupport::Notifications) sur People::*.
  5. Request specs CRUD inline : Events, MembershipTypes, ContributionFormulas.
  6. Edge cases modèles (dates, enums, scopes).

9. CI/CD

RuboCop — rollout progressif (Phase 2+)

  • Baseline : rubocop-rails-omakase dans .rubocop.yml comme seule base héritée ; exclusions projet documentées (permanent / temporaire) + override Ezam Layout/EndOfLine: lf. Le projet est en mode RSpec-only et le dossier legacy test/ est retire.

  • Lot B2 (historique) — le rollout Rails/* a ete fait par petits lots. Aujourd'hui, conserver la meme approche: petites PR, puis bundle exec rubocop + bundle exec rspec verts.

  • Specs et locale : config.i18n.default_locale est :fr ; les messages ActiveRecord / number_to_currency suivent rails-i18n (fr). Dans les tests de validations, préférer I18n.t('errors.messages.blank'), I18n.t('errors.messages.required'), etc., plutôt que du texte anglais codé en dur ; pour les flashes ou sujets de mail, utiliser I18n.t('…') avec la même clé que l’application.

  • Parite FR/EN : config/locales/en.yml est maintenu en parite de cles avec config/locales/fr.yml. Utiliser bundle exec rake i18n:check_keys avant merge.

  • Auth : la stack d'authentification reste le systeme natif Rails 8 ; ne pas introduire Devise.

  • Fallback transitoire : config.i18n.fallbacks = { en: %i[fr] } est volontairement temporaire pendant la complétion de config/locales/en.yml. Objectif long terme : parité fr/en sans fallback implicite pour l’UX anglaise.

  • Parité des clés locales : utiliser bundle exec rake i18n:check_keys pour lister les clés manquantes entre fr.yml et en.yml et prioriser les traductions manquantes.

  • Jobs : .github/workflows/ci-dev.yml (lint, security, test) et .github/workflows/ci-auto-lint.yml — RuboCop est bloquant lorsque la baseline est verte (bin/rubocop --format github --force-exclusion).

  • Élargissement : activer les cops par petits lots, une PR par lot ; pas de refactors métier ni renommage de vocabulaire domaine sans accord (glossaire).

  • Lots suivants suggérés : après vérif des offenses — LOW (Style/TrailingCommaInArrayLiteral / HashLiteral si pertinent), puis MEDIUM (Performance/*, Rails/* au cas par cas), puis HIGH (Lint/* sur flux, Metrics/*, fichiers app/models / app/services sensibles) en revue manuelle uniquement.

  • Commandes locales : même séquence recommandée qu’avant CI — RuboCop puis RSpec (section 6, Workflow lint local) ; ciblage : bundle exec rubocop --only NomDuCop chemins… --force-exclusion.

.github/workflows/01-ci.yml

  • Suite RSpec complète.
  • Génération SimpleCov + check seuil (bloquant si < 12 %).
  • Linting + security audit (bundle audit).

.github/workflows/02-auto-merge-to-staging.yml

  • Trigger après CI réussi sur dev.
  • Merge devstaging puis déploiement automatique.

.github/workflows/03-staging-deploy.yml

  • Suite complète avant déploiement (pas juste smoke tests).
  • Vérification du seuil de couverture.
  • Blocage si tests échouent ou couverture trop basse.

Production

  • Manuelle via le workflow 04-promote-to-main (staging → main).

10. Troubleshooting

CI bloquée par la couverture

  • Vérifier en local : open coverage/index.html.
  • Ajouter les tests manquants si la suite complète passe sous le seuil.
  • Les runs ciblés (bundle exec rspec spec/models/payment_spec.rb) génèrent un rapport mais n'appliquent pas le seuil global.

Tests lents

  • Utiliser bin/test_fast pendant l'itération.
  • Ou bin/test --no-coverage pour gagner ~30 % sur le run.
  • Optimiser les fabriques (préférer build_stubbed quand pas de persistence requise).

Garder la couverture à jour

  • Lancer bin/test après chaque feature.
  • Mettre à jour le badge couverture du README.md lors d'augmentations significatives.

Documentation liée

11. Audit Rails/SkipsModelValidations (classification)

  • risky_bypass (priorité traitée): app/models/user.rb (anonymisation), suivi ciblé app/models/payment.rb.
  • intentional_batch (gardé sous transaction + commentaire explicite): app/services/people/account_merger.rb, app/models/concerns/versionable.rb, app/models/concerns/duplicatable.rb.
  • legacy/archive (exclusion ciblée possible): docs/rake_archive/migrate_to_person_architecture.rake.
  • consolidated_path: la fusion applicative doit passer par People::AccountMerger (éviter les duplications de logique dans d'autres services).
  • identity_invariant: en cible projet, un User est toujours rattaché à une Person (phase de verrouillage en cours).