Až na to, že pokud algoritmus něco složitého "počítá", tak není moc šance na to psát unit test buď tak, že ten algoritmus implementuje někdo jiný/jinak, nebo tak, že se prostě výsledek algoritmu vezme jako že je "správně". Ono to má své opodstatěnění, při refaktoringu/optimalizaci to najde, co to rozbilo (nebo naopak spravilo), ale bohužel chyby to často moc nemá šanci najít.
Jednotkový test nemá nic počítat. Když budu psát jednotkový test na odmocňování, budu testovat, že odmocnina ze dvou vrátí po zaokrouhlení 1,4142 (což si najdu třeba v tabulkách) a odmocnina z -2 vrátí chybu vstupu. Jak se ve skutečnosti odmocnina počítá nemusím pro psaní testu vůbec vědět. Ano, někdy je ten algoritmus tak netypický, že nikde jinde správné výsledky nenajdu, pak opravdu nezbývá, než testem zafixovat výsledky, co ten algoritmus spočítal. Ale to není tak častý případ.
No... taková trapárna jako "null pointer exception". Problém je, že u silného typového systému člověk prostě dělá design tak, aby k té chybě vůbec nemohlo dojít už z principu. U testů je velmi jednoduché zapomenout testovat nějakou funkci na to, zda při nějaké konfiguraci vstupů nespadne na null pointer exception.
„Null pointer exception“ je řekl bych ukázkový příklad. Můžu to ošetřit pomocí typů, pomocí assertů a nebo to můžu testovat. BoneFlute tu asserty řadí do typového systému a dospěl k tomu tak, že začal do typů vkládat přímo testy. Asserty jsou přitom záměrně navržené tak, aby jednoduché případy mohl vyhodnocovat už kompilátor (nebo něco k němu přilepeného). Takže dohadovat se, co přesně je ještě součástí kompilace a co už ne je trochu akademická diskuse, protože to záleží na konkrétní implementaci – právě asserty se obvykle vyhodnocují až za běhu (poprvé tedy typicky v testech), ale stejně tak je může vyhodnocovat už kompilátor.
Takže čistě z praktického hlediska u této konkrétní kategorie chyby bude typický výsledek ten, že typované programy prostě nullpointerexception vyhazovat nebudou, netypované "testované" ano, protože to programátor zapomněl na spoustě míst ošetřit a testy to nenašly.
Myslím, že je to opět spíš teoretický příklad. Z praktického hlediska bude k těm „null pointer exception“ docházet i v těch typových programech, protože ty nefungují ve vzduchoprázdnu, ale komunikují s okolím – a z něj ty NIL hodnoty dostávají. A i tam budou případy, kdy to programátor prostě přetypuje bez kontroly. Krásně je to vidět např. na Kotlinu, který nullable typy zavedl, ale když se někde váže na Javu, je to plné vykřičníků (tj. přetypování nullable typu na non-nullable, ze kterého může vypadnout NullPointerException).
Já to spíš vidím tak, že některé věci prostě není praktické psát do typů (navíc u některých věcí neexistuje třeba jazyk, který by tyhle věci byl schopen vůbec vyjádřit, u souladu se specifikací to IMO u spousty věcí fakt nejde), někdy by z toho i tak bylo víc práce než užitku.... (na druhou stranu, prakticky kdykoliv jsem nepoužil NonEmpty pro neprázdný list, tak se mi to vymstilo).
Souhlasím. A stejně tak souhlasím s tím, že některé věci je naopak dobré mít v typech – třeba rozlišování typů na nullable/non-nullable považuju za hodně užitečné (i když to počet „null pointer exeception“ jen sníží, ale kromě akademických příkladů je nikdy nevymýtí úplně). Nikdy jsem netvrdil, že silnější typový systém nemůže některé testy učinit zbytečnými. Jenom tvrdím, že sebesilnější typový systém nedokáže nahradit všechny jednotkové testy – protože to, že se algoritmus pro nějaký vstup trefil do správného oboru hodnot ještě neznamená, že je výsledek správně.
Ale konkrétně - jak jsem se inspiroval tím LLVM paperem, tak jsem napsal interpret na ten Mapbox style jazyk a běželo to bez testů napoprvé (po cca. 5 hodinách psaní) a doteď jsem v tom kódu interpretu žádnou chybu nenašel (měl jsem jednu chybu mimo interpret, kdy jsem ve 3 ráno omylem napsal && místo ||). A tady je mimo jiné odpověď na týkající se otázky na "méně kódu" - díky těm typům pak při interpretaci není potřeba kontrolovat, jakého typu jsou vstupy funkcí. Protože prostě mají ten správný typ.
Tohle ale dobře funguje na nízké úrovni, v nějakých knihovnách (kde jsou zároveň nejsilnější i jednotkové testy). Jak se dostáváte výš, deklarativní popis typů by se začal komplikovat a srozumitelnější je použít asserty (i když osobně si myslím, že by programátor měl asserty považovat za deklaraci – ale pořád jsou to výrazy). A na ještě vyšších úrovních už se jenom propojují nějaké komponenty, tam už je řešení pomocí typů úplně nereálné (i kdyby jen proto, že ty komponenty vám dodává někdo jiný, koho nedonutíte detailně nadefinovat typy).