Un demo fintech care trece un code review
Un demo devine o simplă machetă când nimeni nu îi poate rula testele. Iată invarianții care merită verificați când se mișcă bani și schela care îi lasă pe un reviewer să cloneze repository-ul și să vadă registrul cum se verifică singur.

Un reviewer care deschide un demo financiar nu îl citește așa cum citește un vizitator o pagină de prezentare. Clonează repository-ul, rulează testele și așteaptă ca registrul să se contrazică. Dacă nu se contrazice, citește mai departe. Dacă testele verifică doar că o rută răspunde cu 200, închide tab-ul. Diferența dintre un demo și o machetă nu stă în finisajul interfeței, ci în faptul că regulile despre bani sunt scrise undeva unde le poate verifica o mașină.
Construim și recenzăm sisteme de acest fel, așa că avem o listă de verificare. Ce urmează este lista, împreună cu schela care face fiecare punct suficient de ieftin încât să reziste și după prima săptămână.
Șase invarianți care merită testați
Un invariant este o propoziție care trebuie să rămână adevărată după fiecare operație, indiferent de intrare. Pentru software care mișcă bani, câțiva dintre ei contează mai mult decât orice funcționalitate și fiecare poate fi testat fără browser.
Fiecare tranzacție însumează zero
O tranzacție în partidă dublă este un set de înregistrări care se anulează reciproc. Dacă vreo cale din cod poate produce o tranzacție cu o singură înregistrare, registrul poate crea bani din greșeală, iar primul care observă va fi un client. Verificarea este o interogare, nu un scenariu: grupează înregistrările pe tranzacție, însumează fiecare grup și afirmă că toate grupurile dau zero. Rulează verificarea pe tot tabelul după un scenariu, nu doar pe conturile atinse de scenariu, pentru că scopul este să prinzi calea la care nimeni nu s-a gândit. Contabilitatea din spatele regulii este explicată în ghidul nostru despre registrul în partidă dublă; aici întrebarea este doar cum o dovedești în integrarea continuă.
Un transfer este atomic: ambele părți sau niciuna
Un test care trimite un transfer și verifică răspunsul testează ruta. Invariantul are nevoie de o eroare inserată la mijloc. Forțează o eroare după debit și înainte de credit, printr-o constrângere încălcată, un trigger care aruncă o excepție sau un stub care eșuează, apoi afirmă că registrul este exact cum era: același număr de înregistrări, aceleași solduri, același număr de tranzacții. Dacă designul nu are niciun punct în care poți introduce o eroare, asta este deja o constatare, pentru că producția va găsi unul.
A doua jumătate este concurența. Pornește două transferuri din același cont în același moment și verifică soldurile finale. Este nevoie de o bază de date care blochează efectiv rândurile. O bază SQLite în memorie, cu o singură conexiune, va trece un test pe care PostgreSQL l-ar respinge, așa că rulează testul pe motorul pe care îl folosești în producție. Ajută și câteva teste bazate pe proprietăți: generează secvențe aleatoare de transferuri și stornări, apoi verifică invarianții după fiecare pas. Scenariile scrise de mână verifică ordinea la care te-ai gândit; cele generate o găsesc pe cea la care nu te-ai gândit.
O stornare creează înregistrări, nu le editează
Apelează endpointul de stornare, apoi verifică patru lucruri: înregistrările originale există în continuare cu valorile neschimbate, s-au scris înregistrări noi, cele noi trimit la tranzacția originală și perechea însumează zero. Un test care verifică doar că soldul a revenit la valoarea anterioară trece și pe o implementare care suprascrie rânduri, adică exact pe implementarea pe care invariantul există ca să o interzică. Adaugă o reîncercare cu aceeași cheie de idempotență și afirmă că numărul de înregistrări nu s-a modificat.
Soldurile sunt derivate, nu stocate ca sursă de adevăr
Două afirmații. Prima: soldul returnat de API este egal cu suma înregistrărilor contului. A doua: dacă există un rând de sold sau un cache, șterge-l, reconstruiește-l din înregistrări și verifică același număr. A doua afirmație este ce separă un cache de o a doua sursă de adevăr și pică exact la sistemele care au nevoie de ea.
Numerele de card nu apar niciodată în loguri
Pregătește un card cu un număr cunoscut, trimite o plată, apoi captează ieșirea de log și afirmă că numărul complet lipsește, în timp ce ultimele patru cifre apar. Fă verificarea pe calea de succes, pe o eroare de validare și pe tabelul de audit. Mesajele de eroare sunt locul unde se scurg cel mai des numerele complete, pentru că programatorul care le scrie vrea să fie de ajutor. Este un test grosolan, o căutare de text în stdout, și prinde greșeala care costă cel mai mult.
O acțiune de operator este atribuibilă
Autentifică-te ca un operator, fă o acțiune privilegiată, apoi afirmă că există o înregistrare de audit cu identitatea operatorului, momentul și valorile dinainte și de după. Apoi autentifică-te ca un client și afirmă că aceeași acțiune este refuzată. Atribuirea eșuează în locuri previzibile: un job programat scrie ca un actor generic până când toate o fac, sau consola de operator ajunge la registru printr-o scurtătură care sare peste scrierea de audit. Pagina unui demo susține aproape mereu că are traseu de audit. Testul este cel care îți spune dacă afirmația este adevărată.
Ce face aceste teste ieftin de scris
Un script de seed care produce o stare cunoscută
Testele au nevoie de numere pe care să le afirme fără să le calculeze cu chiar implementarea testată. Un script de seed creează aceleași conturi, aceleași solduri inițiale și aceiași clienți de fiecare dată, iar testele îl apelează la pregătire, în loc să depindă de un dump exportat cândva. Contează două proprietăți: rulează pe o bază goală și este idempotent, așa că un reviewer îl poate rula de două ori fără să strice starea. Dacă există conturi de test, scrie-le în README și etichetează-le ca date demonstrative. Un reviewer nu ar trebui să citească codul de seed ca să găsească o parolă.
O singură comandă care rulează tot
Pune în README comanda exactă și cum arată succesul, de exemplu câte teste trebuie să treacă și ce linie afișează instrumentul la final. Prima experiență a reviewerului ar trebui să fie o suită verde în mai puțin de un minut. Dacă pregătirea cere o bază de date, un container și două variabile de mediu, scrie cele trei comenzi care fac asta, nu un paragraf care le descrie. Un reviewer nevoit să improvizeze este un reviewer care pleacă.
Teste care verifică registrul, nu HTTP-ul
Păstrează puține teste HTTP, pentru autentificare, validare și coduri de stare, pentru că acelea sunt partea pe care o atinge browserul. Pune afirmațiile despre bani în teste care apelează stratul de servicii și citesc apoi direct tabelul de înregistrări. Rulează mai repede, pică cu un mesaj util și rezistă la o schimbare de format a răspunsului care nu are legătură cu invariantul. O suită care durează minute întregi ajunge să fie sărită; una care durează secunde rulează la fiecare commit.
Date deterministe, inclusiv ceasul
Codul financiar depinde de date: dobânzi, ordine permanente, perioade de extras, expirarea cardurilor. Un test care citește ora curentă pică la sfârșit de lună sau în ziua în care se schimbă ora, iar un test care pică din motive de calendar ajunge marcat ca instabil și apoi ignorat. Trimite un ceas ca parametru în cod, îngheață-l în teste și scrie cel puțin un test care îl mută înainte intenționat, ca să dovedești regula de acumulare.
Un test pe care l-ai văzut picând
Șterge pentru un minut verificarea sumei zero din stratul de servicii și rulează suita. Dacă totul rămâne verde, testul este decor. Facem exercițiul o dată pentru fiecare invariant când recenzăm un demo și este cea mai rapidă cale de a găsi afirmațiile scrise ca să descrie codul, nu ca să îl constrângă.
Un README care spune ce nu face demo-ul
Scrie lipsurile: fără bani reali, fără date reale de card, fără scop PCI, fără întărire pentru producție, adică fără limite de rată, management de secrete, backup sau monitorizare. Nu este modestie. Este paragraful care împiedică un reviewer să presupună că nu ai știut, pentru că lectura alternativă a unui README tăcut este că tu crezi că demo-ul e gata pentru clienți. Limita asumată este și o declarație de scop: îi spune reviewerului ce lipsuri sunt intenționate și transformă restul în întrebări corecte, nu în îndoieli.
Ce verifică un reviewer înainte să citească măcar o linie de cod
Aceste patru puncte durează câteva minute și decid cum va fi citit restul.
- Credențiale în repository. Un
.envcomis, o cheie într-un fixture de test, o parolă într-un fișier compose. Scanarea automată de secrete ajută la un repository mare; la un demo mic, o căutare în istoric este mai rapidă. - Un
.env.examplecare corespunde codului. Fiecare variabilă citită de cod apare în el, nimic din el nu e nefolosit, iar valorile sunt substituenți care eșuează zgomotos dacă cineva rulează cu ele neschimbate. Un exemplu rămas în urmă este dovada că instrucțiunile de instalare nu au fost rulate niciodată de la zero. - Migrații care rulează de la zero. Șterge baza, rulează lanțul de migrații, rulează seed-ul, pornește aplicația. Dacă secvența pică, demo-ul rulează pe un singur laptop.
- Limite asumate. README-ul, licența și dacă instanța publică recunoaște că este un demo. Un demo care pretinde că e produs este cea mai rapidă cale de a pierde un cititor tehnic.
Un exemplu concret
Am publicat unul ca referință deschisă. Danube Bank este un demo bancar în FastAPI și MySQL, cu o aplicație pentru client și o consolă separată pentru operator, cu licență MIT, cu o comandă de seed și un .env.example în ghidul de pornire. Testele lui verifică invarianții registrului, nu ecranele, consola de operator scrie prin același strat de servicii ca aplicația clientului, iar pagina spune deschis că nu există bani reali, carduri reale sau KYC real. Acea ultimă propoziție este prima pe care am căuta-o dacă ar fi scris-o altcineva.
Dacă ai un demo al tău, cea mai ieftină îmbunătățire este un singur test: rulează un scenariu, apoi însumează fiecare tranzacție și afirmă zero. Durează o după-amiază, prinde categoria de bug care face o echipă să se facă de râs în fața unui client și transformă un repository care arată o interfață într-unul care dovedește o afirmație.
Întrebări frecvente
- Ce face un demo fintech credibil pentru cineva care îl recenzează?
- Reviewerul poate clona repository-ul, rula o singură comandă și vedea teste care verifică invarianții banilor, nu ecranele. Adică o bază populată cu numere cunoscute, teste care citesc direct registrul și un README care spune ce nu face demo-ul în mod deliberat. Afirmațiile din interfață contează mai puțin decât o suită care pică atunci când registrul nu se mai echilibrează.
- Ce ar trebui să verifice un test de registru după un transfer?
- Că fiecare tranzacție din tabel însumează zero, că ambele înregistrări au fost scrise în aceeași tranzacție și că soldurile conturilor sunt egale cu suma înregistrărilor lor. Verifică și cazul de eșec: forțează o eroare între debit și credit și afirmă că nimic nu s-a schimbat. Un test care verifică doar răspunsul HTTP dovedește că ruta există, nu că banii s-au mișcat corect.
- Cum testez că numerele de card nu ajung în loguri?
- Pregătește un card cu un număr cunoscut, trimite o plată, captează ieșirea de log și afirmă că numărul complet lipsește, în timp ce ultimele patru cifre apar. Repetă pe o eroare de validare, pentru că mesajele de eroare sunt locul unde se scurg cel mai des numerele complete. Este o căutare de text, nu un test elegant, și se scrie repede.
- De ce contează un script de seed pentru teste?
- Le dă testelor numere pe care le pot afirma fără să le recalculeze cu chiar codul testat. Un seed bun creează aceleași conturi și solduri inițiale de fiecare dată, rulează pe o bază goală și poate fi rulat de două ori fără efecte. Fără el, valorile așteptate sunt calculate de aceeași logică care ar putea fi greșită, iar testele nu mai prind nimic.
- Are nevoie un demo de licență și de o listă de limitări?
- Da, ambele. Licența îi spune cititorului ce poate face cu codul. Limitările îi spun ce nu este codul: fără bani reali, fără date reale de card, fără scop PCI, fără întărire pentru producție. Fără acel paragraf, reviewerul poate presupune doar că nu ai știut de lipsuri sau că socotești demo-ul gata pentru clienți.
- Pot testa atomicitatea cu SQLite în memorie?
- Parțial. SQLite în memorie este bun pentru a verifica faptul că o tranzacție anulată nu lasă înregistrări, dar nu reproduce blocarea de rânduri din PostgreSQL, așa că un test de concurență va trece acolo și va pica în producție. Rulează testele care depind de izolare pe același motor pe care îl folosești la deploy, ideal într-un container pornit de suită.


