Salut à tous ! aujourd’hui on va se pencher à l’intégration continue en PowerShell et plus particulièrement les tests des scripts PowerShell avec le module PowerShell Pester.
Les tests ? on s’en fou… yolo, non?
Alors, non… les tests on s’en passe, certes, bien sur les petits SI, ou pour les bout de scripts en read-only à usage unique. Mais dès que vous commencez à vouloir maintenir votre code dans le temps… Les tests ça devient bel investissement, car si effectivement c’est pas passionnant à écrire, ils permettent quand même de spécifier clairement les entrées et les sorties attendus de vos scripts et donc de s’assurer que celles-ci ne seront pas modifiée lors de vos futures mise à jour.
Donc, pour répondre à la question : dois-je écrire un jeu de tests pour mon code ? j’aurais tendance à dire :
- Si votre code est destiné à avoir une durée de vie supérieure à quelques mois : oui.
- Sinon, c’est à évaluer en fonction de la « criticité » du script que vous comptez exécuter. Exemple : un script qui modifie 150 000 comptes utilisateurs de manière d’un côté, ou de l’autre côté celui test si le port https (TCP/443) est ouvert sur 10 serveurs.
Ça demande un peu de temps, mais le jeu peut valoir la chandelle on on commence à parler MCO, MCS et développement en équipe.
Les types de tests
On peut distinguer 3 principaux types de tests :
- Les tests unitaires : ont vocation à vérifier le fonctionnement des plus petits blocs de code. Dans Powershell, c’est les fonctions (le plus souvent). L’idée étant que lorsque vous écrivez une fonction, vous écrivez également les tests associé de manière à garantir son bon fonctionnement lors de ses évolutions futurs.
- Les tests d’intégration : à l’opposé des tests unitaires, on trouve les tests d’intégration. Ces derniers vont plutôt chercher à tester l’ensemble de votre programme d’un point de vue externe (i.e. mode boite noir). Il sont souvent plus couteux à écrire et à mettre en place car il peuvent nécessiter un environnement complexe.
- Les tests d’acceptation : A l’image de vos tests d’intégrations cette catégorie vérifie le bon fonctionnement « global » du code. Ce sont surtout des tests qui devrait s’exécuter dans l’environnement de production et qui ont vocation à contrôler que tout est normal une fois le code déployé.
Après il en existe une multitude de variantes propre à chaque langage, métier, mode de développement, entreprise, etc. Mais pour ce qu’on va faire aujourd’hui avec PowerShell les 2 premiers suffiront suffira.
Tests des scripts PowerShell avec Pester
La plupart des gros langages de programmation ont leur module de tests : unittest pour python ou testing pour Go par exemple, parfois via des outils tiers comme Travis-CI. PowerShell n’échappe pas à la règle avec le module Pester, dont vous trouverez les sources ici.
Comment ça marche Pester ?
Assez simplement, en gros vous devez définir un bloc de test sous votre code pour définir vos test. Pour illustrer, on va imaginer que je veux écrire des tests unitaires pour ma fonction de génération de mot de passe (qui date un peu). Et dont je vous laisse aller revoir le code, sinon vous allez pas tout capter après.
Tests unitaires des scripts PowerShell
Le premier truc qu’on voudrait tester, c’est que la sortie de notre fonctione correspond bien à un string de la longueur demandée en entrée :
# Ajouter ici le code de Get-SecurityRandomString
# ou dot sourcer le fichier . ./Get-SecurityRandomString.ps1
Describe 'Get-SecurityRandomString' {
Context 'Lenght' {
$TestNumber = get-random -min 12 -max 36
$result = Get-SecurityRandomString -length $TestNumber
it 'return the requested lenght' {
$result.length | should be $TestNumber
}
}
}
Vous pouvez alors lancer votre jeu de tests ainsi :
PS C:\> Invoke-Pester -Script Get-SecurityRandomString Executing script ./Get-SecurityRandomString.ps1 Describing Get-SecurityRandomString Context length [+] return the requested length 1.29s Test completed in 1.29s Tests passed:1, Failed 0, Skip 0, Pending 0, Inconclusive 0
Et voilà comment on fait des tests « simples ». Les éléments que vous devez retenir sont les mots clés suivant :
- Describe : pour déclarer un groupe de test, vous êtes obligé de déclarer un bloc « describe » pour que pester fonctionne
- Context : permet de déclarer un sous-ensemble de tests, ce qui permet entre autre d’organiser vos tests et aussi de gérer le « scope » de vos variables entre vos différents tests.
- it : permet de déclarer un test unitaire, un bloc « It » soit forcément terminer en « failure » ou « passed » en se servant d’un mots clés suivant :
- Should Be, Should Be Exactly ou Should Throw ;
- Sachant que Not est autorisé aussi.
Ce qui vous permet d’écrire des tautologies en langage, presque, naturel :
$true | should not be $false
Les tests plus compliqués
Bon c’est bien, on sait tester des trucs « faciles », mais on pourrait encore améliorer les tests genre tester que l’aléa est de bonne qualité en faisant la moyenne des indices de coïncidence sur plein d’appels et s’assurer que la moyenne est entre bien 2 bornes.
# Ajouter ici le code de Get-SecurityRandomString
# ou dot sourcer le fichier . ./Get-SecurityRandomString.ps1
Describe 'Get-SecurityRandomString' {
function Get-IndiceCoincidence {
[...]
}
Context 'Randomness' {
$ite = 100000
$avg = 0
for($c=0 ; $c -lt $ite; $c++) {
$avg += Get-IndiceCoincidence -$text (Get-SecurityRandomString -length 16)
}
$avg = $avg / $ite
it 'is random enought' {
$avg -gt 0.33 -and $avg -lt 0.43| should be $true
}
}
}
Tests d’intégration des scripts PowerShell
Mais ici on test encore le bon fonctionnement « interne » de notre fonction. La réalité c’est que la plupart du temps un script Powershell repose sur des dépendances externe : Active Directory, NetApp, logiciel tiers, API REST, etc. Or, on a pas forcément envie, ni la possibilité parfois, que nos tests reposent sur ces dépendances externes lors de l’exécution. Par exemple si vos tests créés 150 comptes utilisateurs et les détruisent a chaque exécution : je connais quelques admins AD qui risquent de râler…
Pour continuer sur un exemple, je vais chercher à tester ci dessous la fonction Test-IsThisPasswordHaveBeenPwnd. Mais sans faire un appel réseau au service avec « Invoke-RestMethod -Uri « https://api.pwnedpasswords.com ».
Pour cela on va faire appel au mot clé Mock qui permet, à l’intérieur des tests (donc au moins d’un bloc Describe ou Context), de simuler l’appel à une fonction (ou « surcharger » si vous préférez) . Et comme un exemple vos souvent plus qu’un long discours :
# [Insert code de Test-IsThisPasswordHaveBeenPwnd:
# https://github.com/TiTi87/PSPasswordHaveBeenPwnd/]
Describe "Test-IsThisPasswordHaveBeenPwnd" {
Context "When Password is 'Passw0rd123456'" {
Mock Invoke-RestMethod {
return "00EA5B4A1F59DDC8296ED29F24BBFCD8F43:1
263AFCD372802C5BC5E71A0FC93B70A9828:1
26850297C3AF234196271AFA1DFF7D9BCDC:1
26B9743E72128CDE3370C992A86B49E011E:5
26D17F2E7CEC134698A864C43E5CFBD84ED:50
26F2ECA536A2752D509258781BE4FD08F6A:23
2722CE42DBE70685BBAC0EFADB5E76D98F2:2
276222744A017CFB757924F76814C3C5820:3
2772FF587C44A48B0850F192189EC1CD182:1
27CB1DA8A3EBE5D50F05128739BE0DF287F:49
280D579AB5A7974B27005623093FFEBBC69:5"
}
it "find that the password have been powned" {
Test-IsThisPasswordHaveBeenPwnd -Password 'Passw0rd123456' | should be $true
}
}
Context "When Password is 'Correct Horse Battery Staple'" {
Mock Invoke-RestMethod {
return "1112E7514BC4A89C7F300675BC5D178122C:1
11F509408B11336BFDBEFE9686E1E7A3C7D:2
12235B5CADCB7712B3FEF972CE597A9C51E:1
123473FCCE58D358A16005E731783255FAA:2
137EECE2E17D54388DFB36537FE1DF894E0:1
1383F497EF7CA2164C6681A0D0578827D20:44
13A901CCD6B55903D58C6CC6B9348DEBF21:39
13F5B7D74DE3744F9DEA8C6E4224BD683B5:1
140F91FE3EADCD217647061D07BB15A776F:2"
}
it "find that the password have not been powned" {
$ret = Test-IsThisPasswordHaveBeenPwnd -Password "Correct Horse Battery Staple"
Assert-MockCalled Invoke-RestMethod -Scope It -Time 1
$ret | should be $false
}
}
}
Dans ces tests, notre code ne fera jamais une requête réseau. Les appels seront remplacé par notre code dans le Mock qui simule un retour tel que l’API renverrai dans un fonctionnement normal.
Notez au passage l’appel à Assert-MockCalled qui permet de vérifier que l’appel à Invoke-RestMethod est fait à un certain point. Ici ça ne sert à rien, Ça devient très utile dans le cas où le code prend des embranchement différents : exemple : test suppressions de fichiers.
Cas particulier : tester des modules PowerShell
C’est bien joli tout ça mais jusqu’ici on a toujours écrit le code de nos tests dans nos scripts (ou éventuellement en « dot sourçant » les script dans nos fichier de tests). Sauf que lorsque vous livrer un module vous n’avez pas forcément envie de livrer les tests dans le code du module.
Pester est d’ailleurs un peu sensible avec ça et si vous l’attaquer sans réfléchir vos tests vont échouer. Il vous faudra d’utiliser le mot clé :
InModuleScope
Ou a défaut les fonctions –ModuleName lors de vos appels à Mock. C’est pas bien compliqué au final mais il faut le savoir, je vous invite à aller chercher plus d’infos par ici.
Conclusion
Pour finir, ici on a fait une introduction (pas si) rapide de Pester et des tests des scripts PowerShell. Néanmoins il manque encore plein de trucs que Pester propose pour faire le tour :
- Couverture de code (ou code coverage), voir l’option : –CodeCoverage et –TestName de Invoke-Pester.
- Tags et regroupement de vos tests, cf. : –Tags de Describe et –Tag ou –ExcludeTag de Invoke-Pester. Par exemple pour séparer vos tests de déploiement de ceux de fonctionnement et de gestion au quotidien.
- Construction des environnements de test si des actions sont nécessaires avant ou après vos tests, exemple : si vos test nécessite la création et suppression d’un dossier temporaire. Et là je vous renvois vers : BeforeEach, AfterEach, BeforeAll, and AfterAll
Bref, j’ai encore plein de trucs à vous montrer avec pester. Dont comment faire de l’intégration continue avec vos tests grâce à Azure Pipelines sur Github. Mais du coup c’est pour la prochaine fois ça !
Sources :
Pour info, je me suis basé sur cette série d’article pour écrire 80% ce post. Ne soyez pas surpris si vous voyez des ressemblances !