Typer eller Tester: Varför Inte Båda?

0
45

Då och då, en debatt som blossar upp om värdet av skrivs JavaScript. “Det är bara att skriva fler tester!” skriker några motståndare. “Byt enhet tester med olika typer!” scream andra. Båda har rätt på en del sätt, fel på andra. ger lite utrymme för nyanser. Men i utrymmet i denna artikel kan vi prova att lägga ut en väl underbyggd argumentation för hur både kan och bör samverka.

Korrekthet: vad vi alla egentligen vill ha

Det är bäst att börja i slutet. Vad vi verkligen vill få ut av allt detta meta-teknik på slutet är korrekt. Jag menar inte strikt teoretisk datalogi definition av det, men en mer allmän tillämpning av programmets beteende till sin specifikation: Vi har en idé om hur vårt program borde arbeta i våra huvuden, och processen för programmering organiserar bits och bytes för att göra denna idé till verklighet. Eftersom vi inte alltid är exakt vad vi vill, och eftersom vi skulle vilja ha förtroende för att våra program inte bryta när vi gjort en förändring, vi skriver typer och tester på toppen av rå kod vi redan har för att skriva bara för att få saker att fungera i första hand.

Så, om vi accepterar att korrekthet är vad vi vill ha och typer och tester är bara automatiserade sätt att få det, det skulle vara bra att ha en visuell modell av hur typer och tester för att hjälpa oss att uppnå korrekta, och därför förstår där de överlappar varandra och där de kompletterar varandra.

En visuell modell av programmet korrekthet

Om vi tänker oss att det hela oändligt Turing-kompletta möjliga utrymme för allt program någonsin kan tänkas göra — inclusive misslyckanden — som en stor grå yta, sedan vad vi vill att vårt program för att göra våra specifikation, är en mycket, mycket, mycket liten del av den möjliga utrymme (den gröna diamanten nedan, överdrivna i storlek för tydlighetens skull visar något):

Vårt jobb i planeringen är att tvista vårt program så nära den specifikation som möjligt (att veta, naturligtvis, vi är ofullkomliga, och vår spec är ständigt i rörelse, t ex på grund av mänskliga fel, nya funktioner eller under angivna beteende, så att vi aldrig riktigt lyckas uppnå exakt överlappar varandra):

Obs, igen, att gränserna för vårt program beteende även omfatta planerade och oplanerade fel vid tillämpning av vår diskussion här. Våra innebörden av “korrekt” har planerat fel, men som inte är oplanerade fel.

Tester och Korrekthet

Vi skriver tester för att säkerställa att våra program passar våra förväntningar, men har ett antal val av saker att testa:

Den perfekta tester är de orange prickarna i diagrammet — de noggrant testa att vårt program inte överlappar spec. I denna visualisering, vi vet inte riktigt skilja mellan olika typer av tester, men du kanske kan tänka dig enhetstester som riktigt små prickar, medan integration/end-to-end-tester är stora prickar. Hursomhelst, de är prickar, eftersom ingen test beskriver till fullo varje väg genom ett program. (I själva verket kan du ha 100% täckning i registret och fortfarande inte testa varje bana på grund av kombinatorisk explosion!)

Den blå punkten i detta diagram är ett dåligt test. Visst, det tester som vårt program fungerar, men det spelar faktiskt inte fästa den på underliggande spec (vad vi egentligen vill ha ut av vårt program, i slutet av dagen). Nu fixar vi vårt program för att anpassa närmare spec, detta test bryter, ger oss en falsk positiv.

Den lila prick är en värdefull test eftersom den testar hur vi tror att vårt program ska fungera och identifierar ett område där våra program som för närvarande inte. Ledande med lila tester och fastställande av programmets genomförande i enlighet därmed är även känd som Test-Driven Utveckling.

Den röda testa i detta diagram är en sällsynt test. I stället för det normala (orange) tester för att testa “happy vägar” (inklusive planerade fel staterna), detta är ett test som räknar och kontrollerar att de “olyckliga vägar” att misslyckas. Om detta test “pass” där det ska “misslyckas” det är en enorm tidig varningssignal om att något gick fel — men det är i princip omöjligt att skriva tillräckligt med tester för att täcka vidsträckta möjligt olyckliga vägar som existerar utanför den gröna spec området. Människor sällan finner värde att testa så att saker som inte fungerar, inte fungerar, så att de inte gör det, men det kan fortfarande vara en bra början varningssignal när saker går fel.

Typer och Korrekthet

Där tester är enda poäng på möjligheten utrymme på vad våra program kan göra, typer representerar kategorier carving hela avsnitten från den totala möjliga utrymme. Vi kan se dem som rektanglar:

Vi hämtar en rektangel till en kontrast till den diamant som representerar programmet, eftersom ingen typ-system enbart kan helt beskriva vårt program beteende genom att använda olika typer ensam. (För att välja ett trivialt exempel på detta, ett id-nummer som alltid ska vara ett positivt heltal som är ett nummer typ, men antalet typ accepterar även bråk och negativa tal. Det finns inget sätt att begränsa antalet typ till ett visst intervall, utöver en mycket enkel unionen av antalet strängar.)

Typer tjäna som ett tvång om var våra program kan du gå som du-kod. Om vårt program börjar överstiga den angivna gränserna för dina program typer, vår typ-checker (som Maskin eller Flöde) kommer helt enkelt vägra att låta oss sammanställa vårt program. Detta är bra, eftersom det i ett dynamiskt språk som JavaScript, det är mycket lätt att av misstag skapa en krascha programmet som säkert inte var något du tänkt dig. Den enklaste value add är automatiserad null kontroll. Om foo har ingen metod som kallas bar, då ringer foo.bar() kommer att orsaka alltför välbekanta odefinierad är inte en funktion runtime undantag. Om foo skrev på alla, det kan ha varit fångad av typ-bricka när du skriver, med särskilda tilldelningen till de problematiska i raden av kod (med komplettera automatiskt som en samtidig nytta). Detta är något tester kan helt enkelt inte göra.

Vi kanske vill skriva strikt typer för vårt program så även om vi försöker att skriva den minsta rektangel som fortfarande passar vår spec. Detta har dock en inlärningskurva, för att dra full nytta av typen system handlar om att lära sig ett helt nytt syntax och grammatik av operatörer och generiska typ av logik som behövs för att modellera det fulla dynamiska omfånget av JavaScript. Handböcker och Cheatsheets hjälpa till att sänka denna inlärningskurva, och fler investeringar behövs här.

Lyckligtvis detta antagande/inlärningskurva inte har för att stoppa oss. Eftersom typ-kontroll är en opt-in-process med Flöde och konfigurerbara stränghet med maskinskriven text (med förmåga att selektivt ignorera besvärande rader kod), vi har våra välja från ett spektrum av typ säkerhet. Vi kan även modell, och även detta:

Större rektanglar, som big red one i diagrammet ovan, är en mycket tillåtande antagandet av en typ av system på din kodbas, till exempel, så implicitAny och fullt förlita sig på typ slutsats att bara begränsa vårt program från det värsta i vår kodning.

Måttlig stränghet (som de medelstora grön rektangel) skulle kunna utgöra en mer trogen att skriva, men med massor av utrymningsluckor, som med hjälp av explicita instanser av alla i hela kodbasen och manuell typ påståenden. Fortfarande, de möjliga yta av giltigt program som inte överensstämmer med våra specifikationer som är kraftigt minskas även med detta lätta att skriva arbete.

Maximal stränghet, som den lila rektangel, håller saker och ting så tajt att vår spec att det ibland hittar delar av programmet som inte passar (och dessa är ofta oplanerade fel i ditt program beteende). Att hitta buggar i ett befintligt program som detta är en mycket gemensam historia från lag konvertera vanilj JavaScript codebases. Det är dock få maximalt typ säkerhet ut av vår typ-checker sannolikt innebär att dra nytta av generiska typer och särskilda operatörer utformat för att förfina och minska möjliga utrymme för olika typer för varje variabel och funktion.

Observera att vi inte tekniskt har att skriva vårt program först innan du skriver typer. Efter alla, vi vill bara att våra typer noga modell vår spec, så egentligen kan vi skriva vår första och sedan återfylla genomförandet senare. I teorin skulle detta vara Typ-Driven Utveckling, i praktiken, få personer faktiskt kommer att utveckla detta sätt eftersom typer intimt genomsyra och interleave med vår själva programkoden.

Att sätta ihop dem

Vad vi så småningom bygga upp till är en intuitiv visualisering av hur både typer och i tester som kompletterar varandra för att garantera att våra program är korrekta.

Våra Tester hävda att våra program som särskilt presterar som det är tänkt i välj viktiga vägar (även om det finns vissa andra varianter av tester som diskuterats ovan, den stora majoriteten av tester gör det). I språk av visualisering har vi utvecklat, de “pin-kod” den mörka gröna diamanten av våra program till ljus grön diamant av vår spec. Varje förflyttning bort av våra program som bryter mot dessa tester, vilket gör dem squawk. Detta är utmärkt! Testerna är också oändligt flexibel och konfigurerbar för de flesta anpassade för användningsfall.

Vår Typer hävda att vårt program inte springa iväg från oss genom att blockera eventuella fel lägen bortom en gräns som drar vi, förhoppningsvis så tätt som möjligt runt vår spec. I språk av vår visualisering, de “innehålla” de möjliga drift av våra program bort från vår spec (som vi alltid ofullkomliga, och varje misstag vi gör lägger till ytterligare fel beteende för att vårt program). Typer är också trubbig, men kraftfull (på grund av typ slutledning och redaktör tooling) verktyg för att dra nytta av en stark gemenskap levererar typer du behöver inte skriva från grunden.

I kort:

  • Tester som är bäst på att se glad vägar arbete.
  • Typer som är bäst på att förhindra att olyckliga vägar från befintliga.

Använd dem tillsammans baserat på deras styrkor, för bästa resultat!

Om du vill läsa mer om hur Typer och Tester skär, Gary Bernhardt ‘s utmärkta prata om Gränser och Kent C. Dodds’ Test Trophy var betydande influenser i mitt tänkande för denna artikel.