TDD & TDD sont dans un bateau

TDD au delà des exemples

Arnaud Bailly - @dr_c0d3

Input Output Global

2022-10-21

Introduction

Plan

  • Pourquoi ?
  • TDD par l’exemple
  • Types et Tests de propriété
  • Mutations
  • Conclusion

Présentation

  • Architecte pour la blockchain Cardano chez IOG
  • Traumatisé Intéressé par le test depuis une thèse sur le sujet
  • Pratiquant assidu d’eXtreme Programming
  • Convaincu de l’intérêt du typage statique fort

Pourquoi ?

TDD

TDD

TDD

Développement Dirigé par les Tests

Le problème

Numéro d’Inscription au Répertoire des Personnes Physiques

=

NIR

Le problème

Le problème

Le problème

Le problème

Le problème

Le problème

Le problème

Le problème

Cycle TDD

Cycle TDD

Cycle TDD

Le Code

https://github.com/abailly/xxi-century-typed/

Premier test

validateINSEESpec = describe "Validate INSEE Number" $ do

    it "returns True given a valid INSEE Number" $
        validateINSEE "223115935012322" `shouldBe` True

Une représentation naïve

newtype INSEE1 = INSEE1 String
    deriving newtype (Eq, Show, IsString)

validateINSEE :: INSEE1 -> Bool

Triangulation

    it "must have the right length" $ do
        validateINSEE "2230" `shouldBe` False
        validateINSEE "2230159350123221" `shouldBe` False

Triangulation

    it "first character must be 1 or 2" $
        validateINSEE "323115935012322" `shouldBe` False

Triangulation

    it "characters at index 2 and 3 represent year" $
        validateINSEE "2ab115935012322" `shouldBe` False

Triangulation

    it "characters at index 4 and 5 represent month" $ do
        validateINSEE "223ab5935012322" `shouldBe` False
        validateINSEE "223145935012322" `shouldBe` False
        validateINSEE "223005935012322" `shouldBe` False

Triangulation

    it "characters at index 6 and 7 represent department" $ do
        validateINSEE "22311xx35012322" `shouldBe` False
        validateINSEE "223119635012322" `shouldBe` False

Triangulation

    it "characters at index 6 and 7 contain 99 for a foreign-born person" $
        validateINSEE "200029923123486" `shouldBe` True

Triangulation

    it "characters 8, 9, and 10 represent city or country code" $
        validateINSEE "2231159zzz12322" `shouldBe` False

Triangulation

    it "characters 11, 12, and 13 represent an order" $
        validateINSEE "2231159123zzz22" `shouldBe` False

Triangulation

    it "characters 14 and 15 represent a control key" $ do
        validateINSEE "223115935012321" `shouldBe` False

Le code du validateur

validateINSEE (INSEE1 [gender, year1, year2, month1, month2, dept1, dept2, ...]) =
    validateGender gender
     && validateYear [year1, year2]
     && validateMonth [month1, month2]
     && validateDepartment [dept1, dept2]
validateDepartment :: String -> Bool
validateDepartment dept =
    maybe False (\m -> m <= 95 && m > 0 || m == 99) (readNumber dept)

Après quelques cycles…

Ou pas ?

  • Primitive Obsession anti-pattern
  • Nombre limité d’exemples utilisé pour trianguler
  • On doit revalider à chaque réutilisation d’une valeur de type INSEE

Un meilleur modèle

De l’importance du domaine

Use the Types, Luke

data INSEE = INSEE
    { gender :: Gender
    , year :: Year
    , month :: Month
    , dept :: Department
    , commune :: Commune
    , order :: Order
    }

Use the Types, Luke

La clé ne fait pas partie du modèle, c’est une fonction dérivée

computeINSEEKey :: INSEE -> Key

Use the Types, Luke

data Gender = M | F

data Month = Jan | Fev | Mar | Apr | Mai | Jun ...

Use the Types, Luke

data Department
    = Dept (Zn 96)
    | Foreign

Use the Types, Luke

newtype Year = Year (Zn 10)
    deriving (Eq, Show)

newtype Commune = Commune (Zn 1000)
    deriving (Eq, Show)

newtype Key = Key (Zn 100)
    deriving (Eq, Show)

Développement Dirigé par les Types

Types = Propositions

Types = Propositions

Types = Propositions

Tester aux interfaces

“Parse, Don’t Validate”

©Alexis King

“Parse, Don’t Validate”

parse :: String -> Left ParseError INSEE

Générer des valeurs arbitraires

instance Arbitrary Gender where
    arbitrary = elements [M, F]

instance Arbitrary Year where
    arbitrary = Year <$> someZn

instance Arbitrary Month where
    arbitrary = arbitraryBoundedEnum

instance Arbitrary Department where
    arbitrary = frequency [(9, Dept <$> someZn), (1, pure Foreign)]

Exécution avec QuickCheck

Exécution avec QuickCheck

Exécution avec QuickCheck

newtype Year = Year (Zn 10)
    deriving (Eq, Show)

devrait être

newtype Year = Year (Zn 100)
    deriving (Eq, Show)

Ou pas ?

  • On n’a pas vraiment vérifié la fonction computeKey
  • La propriété vérifie que toute chaîne valide produit une valeur INSEE correcte
  • Mais qu’en-est-il de la contraposée : rejette-t’elle toute chaîne incorrecte ?

Mutations

Idée 🎉

  • Partir d’une valeur INSEE correcte
  • Lui appliquer une mutation arbitraire la transformant en valeur incorrecte
  • Vérifier que parse la rejette

Propriété

inseeValidatorKillsMutants =
    forAll arbitrary $ \insee ->
        forAll genMutant $ \mutation ->
            let mutant = mutation `mutate` insee
                parsedInsee = parse mutant
             in isLeft parsedInsee &
                 tabulate "Mutation"
                   [takeWhile (not . isSpace) $
                     show mutation]

Les mutants

data Mutation = MutateYear {position :: Int, character :: Char}
    deriving (Eq, Show)
mutate :: Mutation -> INSEE -> String

Générateur

genMutant :: Gen Mutation
genMutant = do
  position <- choose (1, 2)
  character <- arbitraryPrintableChar
  pure $ MutateYear{position, character}

Résultat 💣

Générateur amélioré

genMutant = do
  position <- choose (1, 2)
  character <- arbitraryPrintableChar  `suchThat` (not . isDigit)
  pure $ MutateYear{position, character}

Un mutant pour les clés

genMutantForKey :: Gen Mutation
genMutantForKey =
  MutateKey . Key <$> someZn
genMutant =
  oneof [ genMutantForYear, genMutantForKey ]

Résultat 🍾

Conclusion

Types + Tests = 🚀

  • Le typage fort s’intègre parfaitement dans le cycle TDD
  • Le test de propriété évite de devoir énumérer des exemples explicitement
  • L’utilisation de mutations améliore le processus de “triangulation” du domaine
  • Utilisé dans la vraie vie:

Si je fais pas de Haskell ?

  • Tous les langages ont désormais leur bibliothèque de tests de propriété