Picasso: Cum se testează o bibliotecă de componente
Publicat: 2022-03-11O nouă versiune a sistemului de design Toptal a fost lansată recent, ceea ce a impus să facem modificări la aproape fiecare componentă din Picasso, biblioteca noastră de componente internă. Echipa noastră s-a confruntat cu o provocare: cum ne asigurăm că regresiile nu au loc?
Răspunsul scurt este, în mod destul de nesurprinzător, teste. Multe teste.
Nu vom analiza aspectele teoretice ale testării și nici nu vom discuta diferite tipuri de teste, utilitatea acestora și nu vom explica de ce ar trebui să vă testați codul în primul rând. Blogul nostru și alții au tratat deja aceste subiecte. În schimb, ne vom concentra doar pe aspectele practice ale testării.
Citiți mai departe pentru a afla cum scriu dezvoltatorii de la Toptal teste. Depozitul nostru este public, așa că folosim exemple din lumea reală. Nu există abstractizări sau simplificări.
Testarea piramidei
Nu avem o piramidă de testare definită, în sine, dar dacă am face-o, ar arăta astfel:
Piramida de testare a lui Toptal ilustrează testele pe care le subliniem.
Teste unitare
Testele unitare sunt simplu de scris și ușor de rulat. Dacă aveți foarte puțin timp pentru a scrie teste, acestea ar trebui să fie prima alegere.
Cu toate acestea, ele nu sunt perfecte. Indiferent de bibliotecă de testare pe care o alegeți (Jest and React Testing Library [RTL] în cazul nostru), aceasta nu va avea un DOM real și nu vă va permite să verificați funcționalitatea în diferite browsere, dar vă va permite să eliminați îndepărtați complexitatea și testați elementele simple ale bibliotecii dvs.
Testele unitare nu adaugă doar valoare prin testarea comportamentului codului, ci și prin verificarea testabilității generale a codului. Dacă nu puteți scrie teste unitare cu ușurință, sunt șanse să aveți cod prost.
Teste de regresie vizuală
Chiar dacă aveți o acoperire de 100% test unitar, asta nu înseamnă că componentele arată bine pe dispozitive și browsere.
Regresiile vizuale sunt deosebit de dificil de observat cu testarea manuală. De exemplu, dacă eticheta unui buton este mutată cu 1px, va observa un inginer QA? Din fericire, există multe soluții la această problemă a vizibilității limitate. Puteți opta pentru soluții all-in-one de nivel enterprise, cum ar fi LambdaTest sau Mabl. Puteți încorpora plugin-uri, cum ar fi Percy, în testele dvs. existente, precum și soluții DIY de la Loki sau Storybook (care este ceea ce am folosit înainte de Picasso). Toate au dezavantaje: unele sunt prea scumpe, în timp ce altele au o curbă de învățare abruptă sau necesită prea multă întreținere.
Happo la salvare! Este un concurent direct al lui Percy, dar este mult mai ieftin, acceptă mai multe browsere și este mai ușor de utilizat. Un alt mare argument de vânzare? Acceptă integrarea Cypress, care a fost importantă pentru că am vrut să renunțăm la utilizarea Storybook pentru testarea vizuală. Ne-am trezit în situații în care a trebuit să creăm povești doar pentru a ne asigura o acoperire vizuală a testelor, nu pentru că trebuia să documentăm acel caz de utilizare. Asta ne-a poluat documentele și le-a făcut mai greu de înțeles. Am vrut să izolăm testarea vizuală de documentația vizuală.
Teste de integrare
Chiar dacă două componente au teste unitare și vizuale, aceasta nu este o garanție că vor funcționa împreună. De exemplu, am găsit o eroare în care un balon explicativ nu se deschide atunci când este utilizat într-un element derulant, dar funcționează bine atunci când este utilizat singur.
Pentru a ne asigura că componentele se integrează bine, am folosit caracteristica experimentală de testare a componentelor Cypress. La început, am fost nemulțumiți de performanța slabă, dar am reușit să o îmbunătățim cu o configurație personalizată a pachetului web. Rezultatul? Am putut folosi excelentul API al Cypress pentru a scrie teste performante care să asigure că componentele noastre funcționează bine împreună.
Aplicarea piramidei de testare
Cum arată toate acestea în viața reală? Să testăm componenta Acordeonului!
Primul tău instinct ar putea fi să-ți deschizi editorul și să începi să scrii cod. Sfatul meu? Petreceți ceva timp înțelegând toate caracteristicile componentei și scrieți ce cazuri de testare doriți să acoperiți.
Ce să testați?
Iată o defalcare a cazurilor pe care ar trebui să le acopere testele noastre:
- State – Acordeoanele pot fi extinse și restrânse, starea implicită poate fi configurată și această caracteristică poate fi dezactivată
- Stiluri – Acordeoanele pot avea variații de chenar
- Conținut – Se pot integra cu alte unități ale bibliotecii
- Personalizare – Componenta poate avea stilurile suprascrise și poate avea pictograme de extindere personalizate
- Reapeluri – De fiecare dată când starea se schimbă, poate fi invocată un apel invers
Cum se testează?
Acum că știm ce avem de testat, să ne gândim cum să procedăm. Avem trei opțiuni din piramida noastră de testare. Dorim să obținem o acoperire maximă cu o suprapunere minimă între secțiunile piramidei. Care este cel mai bun mod de a testa fiecare caz de testare?
- State – Testele unitare ne pot ajuta să evaluăm dacă stările se schimbă în consecință, dar avem nevoie și de teste vizuale pentru a ne asigura că componenta este redată corect în fiecare stare
- Stiluri – Testele vizuale ar trebui să fie suficiente pentru a detecta regresiile diferitelor variante
- Conținut – O combinație de teste vizuale și de integrare este cea mai bună alegere, deoarece Acordeoanele pot fi utilizate în combinație cu multe alte componente
- Personalizare – Putem folosi un test unitar pentru a verifica dacă un nume de clasă este aplicat corect, dar avem nevoie de un test vizual pentru a ne asigura că componenta și stilurile personalizate funcționează în tandem
- Reapeluri – Testele unitare sunt ideale pentru a vă asigura că sunt invocate apelurile corecte
Piramida de testare a acordeonului
Teste unitare
Suita completă de teste unitare poate fi găsită aici. Am acoperit toate modificările de stare, personalizarea și apelurile inverse:

it('toggles', async () => { const handleChange = jest.fn() const { getByText, getByTestId } = renderAccordion({ onChange: handleChange, expandIcon: <span data-test /> }) fireEvent.click(getByTestId('accordion-summary')) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) fireEvent.click(getByTestId('trigger')) await waitFor(() => expect(getByText(DETAILS_TEXT)).not.toBeVisible()) fireEvent.click(getByText(SUMMARY_TEXT)) await waitFor(() => expect(getByText(DETAILS_TEXT)).toBeVisible()) expect(handleChange).toHaveBeenCalledTimes(3) })Teste de regresie vizuală
Testele vizuale sunt situate în acest bloc de descriere Cypress. Capturile de ecran pot fi găsite în tabloul de bord Happo.
Puteți vedea toate stările diferitelor componente, variantele și personalizările au fost înregistrate. De fiecare dată când se deschide un PR, CI compară capturile de ecran pe care Happo le-a stocat cu cele făcute în filiala dvs.:
it('renders', () => { mount( <TestingPicasso> <TestAccordion /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders disabled', () => { mount( <TestingPicasso> <TestAccordion disabled /> <TestAccordion expandIcon={<Check16 />} /> </TestingPicasso> ) cy.get('body').happoScreenshot() }) it('renders border variants', () => { mount( <TestingPicasso> <TestAccordion borders='none' /> <TestAccordion borders='middle' /> <TestAccordion borders='all' /> </TestingPicasso> ) cy.get('body').happoScreenshot() })Teste de integrare
Am scris un test de „cale proastă” în acest bloc de descriere Cypress care afirmă că acordeonul încă funcționează corect și că utilizatorii pot interacționa cu componenta personalizată. Am adăugat, de asemenea, afirmații vizuale pentru un plus de încredere:
describe('Accordion with custom summary', () => { it('closes and opens', () => { mount(<AccordionCustomSummary />) toggleAccordion() getAccordionContent().should('not.be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() toggleAccordion() getAccordionContent().should('be.visible') cy.get('[data-testid=accordion-custom-summary]').happoScreenshot() }) // … })Integrare continuă
Picasso se bazează aproape în întregime pe GitHub Actions pentru QA. În plus, am adăugat cârlige Git pentru verificările calității codului fișierelor în etape. Am migrat recent de la Jenkins la GHA, așa că configurarea noastră este încă în stadiul de MVP.
Fluxul de lucru este rulat la fiecare modificare din ramura la distanță în ordine secvențială, integrarea și testele vizuale fiind ultima etapă, deoarece sunt cele mai scumpe de rulat (atât în ceea ce privește performanța, cât și costul monetar). Dacă toate testele nu sunt finalizate cu succes, cererea de extragere nu poate fi îmbinată.
Acestea sunt etapele prin care trece GitHub Actions de fiecare dată:
- Instalarea dependenței
- Controlul versiunii – validează că formatul commit-urilor și al titlului PR se potrivesc cu commit-urile convenționale
- Lint – ESlint asigură cod de bună calitate
- Compilare TypeScript - Verificați că nu există erori de tip
- Compilarea pachetelor – Dacă pachetele nu pot fi construite, atunci acestea nu vor fi eliberate cu succes; testele noastre Cypress se așteaptă și la cod compilat
- Teste unitare
- Teste de integrare și vizuale
Fluxul de lucru complet poate fi găsit aici. În prezent, durează mai puțin de 12 minute pentru a parcurge toate etapele.
Testabilitate
La fel ca majoritatea bibliotecilor de componente, Picasso are o componentă rădăcină care trebuie să încapsuleze toate celelalte componente și poate fi folosită pentru a stabili reguli globale. Acest lucru îngreunează scrierea testelor din două motive: inconsecvențe în rezultatele testelor, în funcție de elementele de recuzită utilizate în ambalaj; și boilerplate suplimentară:
import { render } from '@testing-library/react' describe('Form', () => { it('renders', () => { const { container } = render( <Picasso loadFavicon={false} environment='test'> <Form /> </Picasso> ) expect(container).toMatchSnapshot() }) })Am rezolvat prima problemă prin crearea unui TestingPicasso care precondiționează regulile globale pentru testare. Dar este enervant să fii nevoit să o declari pentru fiecare caz de testare. De aceea am creat o funcție de randare personalizată care include componenta trecută într-un TestingPicasso și returnează tot ce este disponibil din funcția de randare a RTL.
Testele noastre sunt acum mai ușor de citit și ușor de scris:
import { render } from '@toptal/picasso/test-utils' describe('Form', () => { it('renders', () => { const { container } = render(<Form />) expect(container).toMatchSnapshot() }) })Concluzie
Configurația descrisă aici este departe de a fi perfectă, dar este un bun punct de plecare pentru cei suficient de aventuroși pentru a crea o bibliotecă de componente. Am citit multe despre testarea piramidelor, dar nu este întotdeauna ușor să le aplici în practică. Prin urmare, vă invit să explorați baza noastră de cod și să învățați din greșelile și succesele noastre.
Bibliotecile de componente sunt unice deoarece deservesc două tipuri de public: utilizatorii finali care interacționează cu interfața de utilizare și dezvoltatorii care folosesc codul dvs. pentru a-și construi propriile aplicații. Investirea timpului într-un cadru robust de testare va aduce beneficii tuturor. Investirea timpului în îmbunătățirea testabilității vă va aduce beneficii ca întreținător și inginerii care vă folosesc (și testează) biblioteca.
Nu am discutat lucruri precum acoperirea codului, testele de la capăt la capăt și politicile de versiune și lansare. Sfatul scurt cu privire la aceste subiecte este: eliberați des, practicați versiunea semantică adecvată, aveți transparență în procesele dvs. și stabiliți așteptări pentru inginerii care se bazează pe biblioteca dvs. Putem revizui aceste subiecte mai detaliat în postările ulterioare.
