Haskell Unit Testing

77

Ich bin neu in Haskell und arbeite an Unit-Tests, finde das Ökosystem jedoch sehr verwirrend. Ich bin verwirrt über die Beziehung zwischen HTF und HUnit.

In einigen Beispielen sehen Sie, wie Sie Testfälle einrichten, in eine Testliste exportieren und dann in ghci mit ausführen runTestsTT(wie in diesem HUnit-Beispiel ).

In anderen Beispielen erstellen Sie einen Testläufer, der in die Kabalendatei eingebunden ist und mithilfe von Präprozessor-Magie Ihre Tests wie in diesem Git-Beispiel findet . Es scheint auch, dass HTF-Tests ein Präfix vorangestellt werden müssen test_oder nicht ausgeführt werden? Es fiel mir schwer, Unterlagen dazu zu finden. Ich bemerkte nur das Muster, das jeder hatte.

Kann mir jemand helfen, das für mich zu klären? Was ist die Standardmethode in Haskell? Was sind die Best Practices? Was ist am einfachsten einzurichten und zu warten?

Devshorts
quelle
Haben Sie sich die QuickCheck-Bibliothek angesehen? Ich fand es immer ziemlich einfach zu bedienen.
bheklilr
4
Ja, aber die schnelle Überprüfung ist ein anderer Anwendungsfall, der für typbasierte Tests gedacht ist, was ich momentan nicht möchte. Es würde mich interessieren, wie ich das auch integrieren kann, wenn ich mich einmal darum gekümmert habe, wie sich htf und hunit verhalten
devshorts
twitter.com/HaskellTips/status/425793151660331008 sagt lieber tastyüber test-framework(HTF?), aber ich sehe auch , dass HTF bekam ein kleines Update letzte Woche, nach mehreren Monaten quiey.
Misterbee

Antworten:

48

Im Allgemeinen wird jedes bedeutende Haskell-Projekt mit Cabal durchgeführt . Dies kümmert sich um das Erstellen, Verteilen, Dokumentieren (mit Hilfe von Schellfisch) und Testen.

Der Standardansatz besteht darin, Ihre Tests in das testVerzeichnis zu stellen und dann eine Testsuite in Ihrer .cabalDatei einzurichten . Dies ist in der Bedienungsanleitung beschrieben . So sieht die Testsuite für eines meiner Projekte aus

Test-Suite test-melody
  type:               exitcode-stdio-1.0
  main-is:            Main.hs
  hs-source-dirs:     test
  build-depends:      base >=4.6 && <4.7,
                      test-framework,
                      test-framework-hunit,
                      HUnit,
                      containers == 0.5.*

Dann in der Datei test/Main.hs

import Test.HUnit
import Test.Framework
import Test.Framework.Providers.HUnit
import Data.Monoid
import Control.Monad
import Utils

pushTest :: Assertion
pushTest = [NumLit 1] ^? push (NumLit 1)

pushPopTest :: Assertion
pushPopTest = [] ^? (push (NumLit 0) >> void pop)

main :: IO ()
main = defaultMainWithOpts
       [testCase "push" pushTest
       ,testCase "push-pop" pushPopTest]
       mempty

Wo Utilsdefiniert einige schönere Schnittstellen über HUnit .

Für Tests mit geringerem Gewicht empfehle ich dringend die Verwendung von QuickCheck . Sie können kurze Eigenschaften schreiben und diese über eine Reihe von zufälligen Eingaben testen. Zum Beispiel:

 -- Tests.hs
 import Test.QuickCheck

 prop_reverseReverse :: [Int] -> Bool
 prop_reverseReverse xs = reverse (reverse xs) == xs

Und dann

 $ ghci Tests.hs
 > import Test.QuickCheck
 > quickCheck prop_reverseReverse
 .... Passed Tests (100/100)
Daniel Gratzer
quelle
6
Wenn ein Projekt wächst, müssen Sie die exportierte Testliste pflegen? Scheint fehleranfällig. Ich glaube, ich bin immer noch verwirrt darüber, wie dies mit der automatisch exportierten Präprozessormethode zusammenhängt. Ich sehe viele Unit-Test-Beispiele, aber es ist alles anders
Devshorts
@devshorts In der Liste der Tests können Sie jeden Test einzeln benennen. Ich glaube, dass es Frameworks gibt, die Ihre Tests automatisch ausführen, aber ich habe normalerweise ungefähr 10 Tests pro Datei, so dass das Verwalten derart kleiner Listen recht einfach ist.
Daniel Gratzer
1
Können Sie vielleicht erläutern, wie Sie diese Methode mit mehr als einer Testdatei verwenden? Ist der Hauptläufer separat oder normalerweise Teil der Testvorrichtung?
Devshorts
1
@devshorts Ich habe meine Tests aufgeteilt und von jedem Modul die Liste der Tests mit ihren Namen exportiert. Dann kombiniere ich hauptsächlich die Listen und führe sie aus.
Daniel Gratzer
Ich habe diese Antwort verwendet, um das Test-Framework für dieses kleine Projekt zu schreiben: github.com/siddharthist/m3u-convert Dank an @jozefg, und ich hoffe, das Beispiel hilft allen :-)
Langston
34

Ich bin auch ein Neuling in Sachen Haskeller und fand diese Einführung sehr hilfreich: " Erste Schritte mit HUnit ". Zusammenfassend werde ich hier ein einfaches Testbeispiel für die Verwendung von HUnit ohne .cabalProjektdatei vorstellen :

Nehmen wir an, wir haben ein Modul SafePrelude.hs:

module SafePrelude where

safeHead :: [a] -> Maybe a
safeHead []    = Nothing
safeHead (x:_) = Just x

Wir können Tests TestSafePrelude.hswie folgt durchführen:

module TestSafePrelude where

import Test.HUnit
import SafePrelude

testSafeHeadForEmptyList :: Test
testSafeHeadForEmptyList = 
    TestCase $ assertEqual "Should return Nothing for empty list"
                           Nothing (safeHead ([]::[Int]))

testSafeHeadForNonEmptyList :: Test
testSafeHeadForNonEmptyList =
    TestCase $ assertEqual "Should return (Just head) for non empty list" (Just 1)
               (safeHead ([1]::[Int]))

main :: IO Counts
main = runTestTT $ TestList [testSafeHeadForEmptyList, testSafeHeadForNonEmptyList]

Jetzt ist es einfach, Tests durchzuführen mit ghc:

runghc TestSafePrelude.hs

oder hugs- muss in diesem Fall TestSafePrelude.hsumbenannt werden in Main.hs(soweit ich mit Umarmungen vertraut bin) (vergessen Sie nicht, auch den Modul-Header zu ändern):

runhugs Main.hs

oder irgendein anderer haskellCompiler ;-)

Natürlich gibt es noch mehr als das HUnit, daher empfehle ich wirklich, das empfohlene Tutorial und das Benutzerhandbuch für die Bibliothek zu lesen .

paluh
quelle
1
Hallo, Link zum Erste Schritte mit HUnit ist kaputt
Sandwood
4

Sie haben Antworten auf die meisten Ihrer Fragen erhalten, aber Sie haben auch nach HTF gefragt und wie das funktioniert.

HTF ist ein Framework, das sowohl für Unit-Tests entwickelt wurde - es ist abwärtskompatibel mit HUnit (es integriert und umschließt es, um zusätzliche Funktionen bereitzustellen) - als auch für eigenschaftsbasierte Tests - es lässt sich in Quickcheck integrieren. Es verwendet einen Präprozessor, um Tests zu lokalisieren, sodass Sie keine Liste manuell erstellen müssen. Der Präprozessor wird Ihren Testquelldateien mithilfe eines Pragmas hinzugefügt:

{-# OPTIONS_GHC -F -pgmF htfpp #-}

(Alternativ könnten Sie Ihrem ghc-optionsEigentum in Ihrer Kabalendatei die gleichen Optionen hinzufügen , aber ich habe dies noch nie versucht, weiß also nicht, ob es nützlich ist oder nicht).

Der Präprozessor durchsucht Ihr Modul nach Funktionen der obersten Ebene mit dem Namen test_xxxxoder prop_xxxxund fügt sie einer Liste von Tests für das Modul hinzu. Sie können diese Liste entweder direkt verwenden, indem Sie eine mainFunktion in das Modul einfügen und sie ausführen ( main = htfMain htf_thisModuleTests) oder sie aus dem Modul exportieren und ein Haupttestprogramm für mehrere Module erstellen, das die Module mit Tests importiert und alle ausführt:

import {-@ HTF_TESTS @-} ModuleA
import {-@ HTF_TESTS @-} ModuleB
main :: IO ()
main = htfMain htf_importedTests

Dieses Programm kann mit der von @jozefg beschriebenen Technik in cabal integriert oder in ghci geladen und interaktiv ausgeführt werden (obwohl nicht unter Windows - Einzelheiten finden Sie unter https://github.com/skogsbaer/HTF/issues/60 ).

Tasty ist eine weitere Alternative, mit der verschiedene Arten von Tests integriert werden können. Es hat keinen Präprozessor wie HTF, aber ein Modul, das ähnliche Funktionen mit Template Haskell ausführt . Wie bei HTF wird auch hier die Namenskonvention verwendet, um Ihre Tests zu identifizieren (in diesem Fall case_xxxxeher als test_xxxx). Neben HUnit- und QuickCheck-Tests enthält es auch Module zur Behandlung einer Reihe anderer Testtypen.

Jules
quelle