Neulich hat mich ein Kollege auf ein nettes kleines Tool namens Pester aufmerksam gemacht. Es ist ein nützliches Werkzeug, um PowerShell-Codes zu validieren und sauberer zu skripten.
Index
Was ist Pester?
Pester ist ein Framework, um Tests für PowerShell-Skripte zu definieren.
Die Idee hierbei ist grob gesagt, dass man
- ERST definiert, was ein Skript bzw. eine bestimmte Funktion machen soll
- bevor man die eigentliche Funktion schreibt.
Pester baut automatisch ein “Skelett” für die neue Funktion und legt zusätzlich ein File für die notwendigen Tests an. Die Tests selbst können fast in natürlicher Sprache geschrieben werden, sodass das Ganze wirklich sehr einfach ist und auch von nicht-Profis genutzt werden kann.
Man beschreibt seine Funktion indem man sagt, was bei einem bestimmten Input für ein Output zu erwarten ist. Da man zuerst die Definition schreibt, bevor man an den eigentlichen Code geht, werden (und sollen) zunächst alle Tests fehlschlagen. Während man dann sein Skript oder seine Funktion um immer mehr geplante Features erweitert, werden immer mehr Tests erfolgreich durchlaufen. Dies ermöglicht es einem während der Entwicklung immer wieder zu prüfen, ob man auch das entwickelt, was geplant war.
Einfache Installation
Pester erhält man zum kostenfreien Download über GitHub.
Die Installation unter Windows 10 ist denkbar einfach, da es bereits vorinstalliert ist. Trotzdem wird empfohlen, ein Update zu machen:
1 |
Install-Module -Name Pester -Force -SkipPublisherCheck |
Dieses Kommando benötigt erhöhte Rechte und sollte in einer PowerShell ausgeführt werden, welche als Administrator gestartet wurde. Außerdem wird eine Internetverbindung benötigt.
Für ältere Windows-Versionen sollte dieses Kommando auch funktionieren, solange NuGet vorhanden ist.
Erstellen der Projekt-Dateien
Damit man nicht alles von Hand machen muss, bietet Pester einem die Möglichkeit, automatisch über einen einzigen Befehl alle notwendigen Dateien für unser neues Projekt zu erstellen.
Mit dem Befehl
1 |
New-Fixture -Path <Neues Unterverzeichnis> -Name <Name des neuen Skripts> |
<Name des neuen Skripts>.ps1
<Name des neuen Skripts>.Test.ps1
Die erste Datei enthält nur eine leere Funktion, welche auch so heißt, wie das Skript selbst.
Die zweite Datei beinhaltet das Gerüst für unsere Tests:
Describe-Block zur Beschreibung der Funktion
Das wichtige hier ist der Block “Describe”. Dieser beschreibt die Funktion “get-validUser”, welche sich in unserem PowerShell-Skript “get-validUser.ps1” befindet:
Sehen wir uns einmal den Block genauer an:
Für jede Funktion gibt es einen Describe-Block, welcher die Funktion beschreibt. Eine Funktion wird über It-Blöcke beschrieben. Jeder It-Block beschreibt eine bestimmte Eigenschaft einer Funktion. Der String direkt neben dem “It” ist ähnlich wie ein Kommentar zu sehen und dient nur dazu dem Entwickler klarzumachen, was die Funktion machen soll. Der eigentliche Spaß spielt sich innerhalb des It-Blocks ab.
Jeder It-Block ist in zwei Teile geteilt, welche durch einen Pipe getrennt sind. Links von dem Pipe steht in der Regel ein Aufruf der zu beschreibenden Funktion mit einem bestimmten Input. Die Funktion wird während des Testlaufs aufgerufen und liefert einen Output. Dieser Output wird (über das Pipe) an den Teil rechts weitergegeben. Rechts des Pipes wartet dann das Kommando “Should”, welches den Output entgegennimmt. “Should” ist Teil von Pester und dient dazu den tatsächlichen Output gegen einen erwarteten Output zu prüfen. Welcher das ist, wird über weitere Keywords festgelegt. In der Beispieldatei ist dieses “Be”. Zu lesen ist das Ganze dann so:
“WAHR ($true) sollte FALSCH ($false) sein”.
Da $true natürlich NICHT $false ist, wird dieser Test fehlschlagen.
Um einen Testlauf mit Pester zu starten, müssten wir nur “Invoke-Pester” ausführen und Pester testet automatisch alle Testdateien.
Wie wir sehen, erwartet Pester “$false”, da es rechts des Pipes steht und der tatsächliche Wert ist $true.
Beispiel: Beschreibung eines validen AD-Benutzers
Wir wollen in unserem Beispiel eine Funktion schreiben, welche uns einen “validen” AD-Benutzer zurückgibt. Wir möchten mit Pester beschreiben, was einen validen Benutzer ausmacht und prüfen, ob die Funktion das tut, was wir erwarten.
Wie gesagt, beginnen wir zunächst damit zu beschreiben, was für uns einen validen AD-Benutzer ausmacht.
Anforderung an die Funktion
Unser Benutzer soll:
- Ein AD-Benutzer sein
- Einen Vor- und Nachnamen haben
- Der cn und sAMAccountName sollen identisch sein
- Der Benutzer soll nicht deaktiviert sein
Für den ersten Punkt müssen wir den Typ des Rückgabewertes überprüfen. Dieser sollte “Microsoft.ActiveDirectory.Management.ADUser” sein, da dies der Objekttyp ist, welcher das PowerShell Commandlet “Get-ADUser” zurückgibt.
Im zweiten Test müssen wir sichergehen, dass die Attribute “sn” und “givenName” nicht leer sind.
Im Anschluß, für den dritten Test, vergleichen wir die Attribute “cn” und “sAMAccountName”. Hierfür werden wir sowohl links, als auch rechts des Pipes dieselbe Funktion aufrufen!
Für den letzten Test müssen wir in die UserAccountControl im AD schauen. Da dies etwas umständlich ist und wir es vielleicht auch später noch einmal benutzen möchten, sehen wir hierfür schon mal eine weitere Funktion vor, welche wir aufrufen.
Beschreibung der Funktion als Skript
Die fertige Beschreibung unseres Skripts sieht dann so aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
$here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path)-replace '\.Tests\.', '.' ."$here\$sut" $userName = "fa-siefert" Describe "get-ValidUser" { It "gets an AD user" { (get-validUser -username $userName).GetType() | Should Be Microsoft.ActiveDirectory.Management.ADUser } It "should have a surname" { (get-validUser -username $userName).sn | Should not BeNullOrEmpty } It "should have a given name" { (get-validUser -username $userName).givenName | Should not BeNullOrEmpty } It "should have a matching cn an sAMAccountname" { (get-validUser -username $userName).cn | Should BeExactly (get-validUser -username $userName).sAMAccountName } It "should not be disabled" { is-enabled (get-validUser -username $userName).userAccountControl | Should Be $true } } Describe "is-enabled" { It "Should return true, when a user is enabled" { is-enabled 512 | Should Be $true } It "should return false, when a user is enabled" { is-enabled 514 | Should Be $false } } |
Dies sind alles Definitionen, die wir treffen können, ohne auch nur eine Zeile Code geschrieben zu haben.
Erster Testlauf, um Funktion zu verifizieren
Die Variable $userName ist Teil unserer Test-Definition. Wir können mit ihr verschiedene User ausprobieren, gegen die wir testen möchten.
Wir haben auch schon direkt unsere Helferfunktion beschrieben, welche uns sagen wird, ob ein User enabled oder disabled ist im AD. Die Werte 512 und 514 sind Beispiele. Wichtig ist bei dem Test, ob das zweite Bit von rechts in der UserAccountControl auf 1 steht oder nicht. Bei 512 wäre das der Fall, bei 514 nicht.
Wir können jetzt einmal einen Testlauf machen:
Interessanterweise ist sogar ein Test erfolgreich gewesen! Naja, das liegt aber daran, dass sowohl unser cn, als auch unser saMAccountName leer ist :).
Der Test hat aber funktioniert!
Erstellung des Skriptes
Wir können jetzt anfangen, unser eigentliches Skript zu schreiben.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function get-ValidUser { param($userName) $userAD = Get-ADUser $userName -Properties sn, givenName, sAMAccountName, cn, userAccountControl return $userAD } function is-enabled { param($userAccountControl) if(($userAccountControl -band 2) -eq 0) { # Check if user is enabled (second bit from the right of the userAccountControl) return $true } else { return $false } } |
Die Funktion “get-ValidUser” ruft das Kommando “Get-ADUser” auf und holt sich einen User im Active Directory mit den Eigenschaften, welche hinter “-Properties” stehen. Das User-Objekt wird dann zurückgegeben.
Die Funktion “is-enabled” prüft anhand einer gegebenen UserAccountControl, ob ein bestimmter User aktiviert ist oder nicht. Hierfür wird, wie gesagt, das zweite Bit von rechts geprüft.
Prüfen der Eigenschaften des Active Directory Users
Wenn wir jetzt die Tests noch einmal durchlaufen, sehen wir, dass nun alles grün ist:
Zum Beweis hier einmal die Eigenschaften des AD Benutzers:
Die Funktion gibt also einen AD-User zurück, welcher einen Vor- und Nachnamen hat, einen identischen cn und sAMAccountName und aktiviert ist.
Natürlich ist das alles nur ein Test und wenn man hier einen anderen User angibt, welcher nicht die gewünschten Eigenschaften hat, schlagen die Tests fehl:
Warum das Ganze?
Das Beispiel hier war natürlich etwas konstruiert, aber es sollte trotzdem die Vorgehensweise und die Idee bei der Verwendung von Pester wiedergeben.
Pester bietet nicht nur eine einfache Möglichkeit, seinen Code zu validieren. Es hilft auch dabei, besser zu planen und damit von Anfang an sauberer zu programmieren. Indem man sich erst Gedanken macht, was eigentlich in einem Skript passieren soll und was der gewünschte Output bei einem bestimmten Input ist. Man kann dies alles definieren, ohne auch nur eine Zeile Code zu schreiben. Hat man dann seine Festlegungen getroffen, kann man loslegen und direkt wieder verifizieren, ob man es richtig gemacht hat. Weiterhin kann man so auch leicht sicherstellen, dass man bei einem Update nichts kaputt macht, was vorher funktioniert hat.
Die Tests können beliebig umfangreich sein, sodass man sich keine Sorgen machen muss, es zu übertreiben.
Auch ist es empfehlenswert, zunächst auch bei Anpassungen oder neuen Features erst die Test-Cases in Pester zu beschreiben, bevor man diese einbaut. So lässt sich immer das Gesamtpaket verifizieren.
Das PowerShell Test-Framework Pester ist Open Source und kann einfach über GitHub heruntergeladen werden: https://github.com/pester/Pester (⇨ GitHub).
FirstAttribute AG – Ihr Microsoft Consulting Partner
Wir unterstützen Sie bei PowerShell Scripting Fragen.
Nehmen Sie Kontakt zu uns auf.
Leave a Reply
<p>Danke für Ihre Anregungen, Fragen und Hinweise.<br/>Infos zum <a href="https://www.active-directory-faq.dekontakt/">Datenschutz</a></p>