2012-04-05 22 views
7

Mam wiele funkcji, takich jak: method1, method2, method3. Dla wszystkich z nich istnieją funkcje testowe typu HUnit, takie jak: testMethod1, testMethod2, testMethod3.pobierz nazwę funkcji wewnątrz niego

testMethod1 = TestCase $ 
    assertEqual "testmethod1" ... 

testMethod2 = TestCase $ 
    assertEqual "testmethod2" ... 

testMethod3 = TestCase $ 
    assertEqual "testmethod3" ... 

Chciałbym uniknąć nadmiarowe kopiowanie nazwa funkcji jako prefiksu błąd wiadomości i nazywają to coś takiego:

testMethod1 = TestCase $ 
    assertEqual_ ... 

Jak to może być osiągnięte (dowolny „magia” Sztuką jest doceniana)?

Tak naprawdę pytanie brzmi, w jaki sposób nazwa funkcji może zostać użyta w jego definicji?


Aktualizacja.

To nie jest faktycznie wynika z pierwotnego pytania, które chcę obsłużyć tego typu sytuacji zbyt:

tProcess = TestCase $ do 
    assertEqual "tProcess" testResult $ someTest 
    assertEqual "tProcess" anotherTestResult $ anotherTest 
    assertEqual "tProcess" resultAgain $ testAgain 

Wreszcie chcę napisać coś takiego:

tProcess = TestCase $ do 
    assertEqual_ testResult $ someTest 
    assertEqual_ anotherTestResult $ anotherTest 
    assertEqual_ resultAgain $ testAgain 
+1

Szablon haskell – pat

+2

Moje stare pytanie może być przydatne: http://stackoverflow.com/questions/7896928/how-to-get-variable-name- in- haskell –

Odpowiedz

10

nie można zrobić to bezpośrednio (tj. tak, aby Twoja walizka testowa zaczynała się od testMethodN = ...), ale możesz użyć numeru Template Haskell, aby otrzymać:

testCase "testMethod1" [| do 
    assertEqual_ a b 
    assertEqual_ c d 
|] 

Wymaga to napisania testCase :: String -> Q Exp -> Q [Dec], funkcji zmieniającej nazwę przypadku testowego i wyrażenie w cudzysłowy na listę deklaracji. Na przykład:

{-# LANGUAGE TemplateHaskell #-} 
     
import Data.Char 
import Control.Applicative 
import Control.Monad 
import Language.Haskell.TH 
import Data.Generics 

assertEqual :: (Eq a) => String -> a -> a -> IO() 
assertEqual s a b = when (a /= b) . putStrLn $ "Test " ++ s ++ " failed!" 

assertEqual_ :: (Eq a) => a -> a -> IO() 
assertEqual_ = error "assertEqual_ used outside of testCase" 

testCase :: String -> Q Exp -> Q [Dec] 
testCase name expr = do 
    let lowerName = map toLower name 
    e' <- [| assertEqual lowerName |] 
    pure <$> valD 
        (varP (mkName name)) 
        (normalB (everywhere (mkT (replaceAssertEqual_ e')) <$> expr)) 
        [] 
  where 
    replaceAssertEqual_ e' (VarE n) | n == 'assertEqual_ = e' 
    replaceAssertEqual_ _ e = e 

Podstawową ideą jest, aby wygenerować definicję nazwy danego, i zastąpić wszystkie wystąpienia zmiennej assertEqual_ w cytowanej wypowiedzi z assertEqual lowerName. Dzięki obsłudze szablonu Haskell Scrap Your Boilerplate nie musimy przechodzić przez całą AST, wystarczy określić transformację dla każdego węzła Exp.

Należy pamiętać, że assertEqual_ musi być identyfikatorem związanym z poprawnym typem, ponieważ wyrażenie cytowane jest typowane przed przekazaniem do testCase. Dodatkowo, testCase musi być zdefiniowany w oddzielnym module niż ten, w którym jest używany, ze względu na ograniczenie etapu GHC.

+1

Oznacza to, że odbicie nie jest obsługiwane? –

+1

@Riccardo: Umożliwienie ci dostępu do nazwy funkcji z wewnątrz byłoby raczej nieczyste. Nawet w języku Lisp, języku znanym z metaprogramowania, rozwiązaniem jest użycie obiektu makro (w tym przypadku szablonu Haskell). – ehird

+1

Zaktualizuj moje pytanie. –

1

Istniejące odpowiedzi wyjaśniają, jak to zrobić z metaprogramowaniem, ale jednym ze sposobów uniknięcia jest testowanie anonimowe, które bierze ich nazwę jako argument.

Możemy wtedy użyć Data.Map powiązać je z ich nazwami (w tym przypadku jestem po prostu stosując surowe twierdzenia, plus trochę cukru składniowej z pakietu map-syntax):

import Data.Map 
import Data.Map.Syntax 
import Test.HUnit 

assertEqual_ x y n = assertEqual n x y 

Right tests = runMap $ do 
    "test1" ## assertEqual_ 1 2 
    "test2" ## assertEqual_ 1 1 
    "test3" ## assertEqual_ 3 2 

Aby uruchomić te, my może złożyć Data.Map przy użyciu funkcji, które:

  • przybiera imię i twierdzenie-oczekiwania-for-a-name jako argumenty
  • Przekazuje nazwę na twierdzenie-oczekiwania-for- a-name
  • Przekazuje otrzymany Assertion do TestCase
  • Uruchamia TestCase
  • wiąże inny monadycznej działania, wykorzystując >>

Używamy return() jak nasz domyślny monadycznej działania:

runTests = foldWithKey go (return()) tests 
    where go name test = (runTestTT (TestCase (test name)) >>) 

Daje to wyniki takie jak:

> go 
### Failure: 
test1 
expected: 1 
but got: 2 
Cases: 1 Tried: 1 Errors: 0 Failures: 1 
Cases: 1 Tried: 1 Errors: 0 Failures: 0 
### Failure: 
test3 
expected: 3 
but got: 2 
Cases: 1 Tried: 1 Errors: 0 Failures: 1