packages feed

hspec-tables (empty) → 0.0.1

raw patch · 7 files changed

+450/−0 lines, 7 filesdep +basedep +hspecdep +hspec-core

Dependencies added: base, hspec, hspec-core, hspec-tables

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog++`hspec-tables` uses [PVP Versioning][1].+The changelog is available [on GitHub][2].++## 0.0.1++* Initially created.++[1]: https://pvp.haskell.org+[2]: https://github.com/marcin-rzeznicki/hspec-tables/releases
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2020 Marcin Rzeźnicki++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ README.md view
@@ -0,0 +1,99 @@+# hspec-tables++[![Hackage](https://img.shields.io/hackage/v/hspec-tables.svg?logo=haskell)](https://hackage.haskell.org/package/hspec-tables)+[![Stackage Lts](http://stackage.org/package/hspec-tables/badge/lts)](http://stackage.org/lts/package/hspec-tables)+[![Stackage Nightly](http://stackage.org/package/hspec-tables/badge/nightly)](http://stackage.org/nightly/package/hspec-tables)+[![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)+[![Build Status](https://travis-ci.com/marcin-rzeznicki/hspec-tables.svg?branch=master)](https://travis-ci.com/marcin-rzeznicki/hspec-tables)++`Test.Hspec.Tables` allows you to define table-driven (or, _by-example_) [HSpec](https://hspec.github.io/) tests. For example:++```haskell+import Test.Hspec+import Test.Hspec.Tables++example1 :: Spec+example1 =+  describe "multiplication table" $+    byExample+      ("x", "y", "result")+      [ (0, 0, 0),+        (0, 1, 0),+        (0, 2, 0),+        (1, 0, 0),+        (1, 1, 1),+        (1, 2, 2),+        (2, 0, 0),+        (2, 1, 2),+        (2, 2, 4)+      ]+      (\a b expected -> a * b == expected)+```++or++```haskell+example2 :: Spec+example2 =+  describe "reverse" $+    byExample+      ("list", "reversed")+      [("abc", "cba"), ("", ""), ("0123456", "6543210")]+      (shouldBe . reverse)+```++When you run these, you'll see that each row becomes a seperate test labelled with `show row` (so the requirement for rows is to consist of elements that have a `Show` instance):++```+Example specs+  multiplication table+    ("x","y","result")+      (0,0,0)+      (0,1,0)+      (0,2,0)+      (1,0,0)+      (1,1,1)+      (1,2,2)+      (2,0,0)+      (2,1,2)+      (2,2,4)+  reverse+    ("list","reversed")+      ("abc","cba")+      ("","")+      ("0123456","6543210")++Finished in 0.0008 seconds+12 examples, 0 failures++```++The `byExample` method is type-safe. The table _header_ must be a 2- to 7- element tuple of String and you must provide the same number of "columns" or it doesn't compile. So if you attempted to write:++```haskell+example1 :: Spec+example1 =+  describe "THIS EXAMPLE DOES NOT COMPILE" $+    byExample+      ("x", "y", "result") -- 3 columns+      [ (0, 0, 0, 0) ]     -- 4 columns+      (\a b c expected -> a * b * c == expected)+```++then you should get the following error, hopefully guiding to what the problem is++```+error:+    • Couldn't match type ‘([Char], [Char], [Char])’+                     with ‘(String, String, String, String)’+      Expected type: Test.Hspec.Tables.Header+                       (Integer, Integer, Integer, Integer)+        Actual type: ([Char], [Char], [Char])++```++The assertion function will match the table type, so if your table is of shape `(a, b, c)` then the assertion is assumed to be of type `(Example e) => a -> b -> c -> e` (ie. it's always curried). [Example](https://hackage.haskell.org/package/hspec/docs/Test-Hspec.html#t:Example) comes from `HSpec`++## Caveats++* You can define tables up-to 7 columns (adding columns beyond that requires providing an instance of the `Table` type-class)
+ hspec-tables.cabal view
@@ -0,0 +1,61 @@+cabal-version:       2.4+name:                hspec-tables+version:             0.0.1+synopsis:            Table-driven (by-example) HSpec tests+description:         Table-driven (by-example) HSpec tests+homepage:            https://github.com/marcin-rzeznicki/hspec-tables+bug-reports:         https://github.com/marcin-rzeznicki/hspec-tables/issues+license:             MIT+license-file:        LICENSE+author:              Marcin Rzeźnicki+maintainer:          Marcin Rzeźnicki <marcin.rzeznicki@gmail.com>+copyright:           2020 Marcin Rzeźnicki+category:            Testing+build-type:          Simple+extra-doc-files:     README.md+                     CHANGELOG.md+tested-with:         GHC == 8.8.2++source-repository head+  type:                git+  location:            https://github.com/marcin-rzeznicki/hspec-tables.git++common common-options+  build-depends:       base ^>= 4.13.0.0,+                       hspec-core ==2.7.*+  +  ghc-options:         -Wall+                       -Wno-type-defaults+                       -Wcompat+                       -Widentities+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -fdefer-typed-holes+  if impl(ghc >= 8.0)+    ghc-options:       -Wredundant-constraints+  if impl(ghc >= 8.2)+    ghc-options:       -fhide-source-paths+  if impl(ghc >= 8.4)+    ghc-options:       -Wmissing-export-lists+                       -Wpartial-fields+  if impl(ghc >= 8.8)+    ghc-options:       -Wmissing-deriving-strategies++  default-language:    Haskell2010++library+  import:              common-options+  hs-source-dirs:      src+  exposed-modules:     Test.Hspec.Tables++test-suite hspec-tables-test+  import:              common-options+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  other-modules:       ExampleSpecs+  build-depends:       hspec-tables,+                       hspec ==2.7.*+  ghc-options:         -threaded+                       -rtsopts+                       -with-rtsopts=-N
+ src/Test/Hspec/Tables.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE AllowAmbiguousTypes #-}+{-# LANGUAGE DefaultSignatures #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}++-- |+-- Copyright: (c) 2020 Marcin Rzeźnicki+-- SPDX-License-Identifier: MIT+-- Maintainer: Marcin Rzeźnicki <marcin.rzeznicki@gmail.com>+--+-- Table-driven (by-example) HSpec tests.+--+-- Example usage:+--+-- > describe "multiplication table" $+-- >  byExample+-- >    ("x", "y", "result")+-- >    [ (0, 0, 0),+-- >      (0, 1, 0),+-- >      (0, 2, 0),+-- >      (1, 0, 0),+-- >      (1, 1, 1),+-- >      (1, 2, 2),+-- >      (2, 0, 0),+-- >      (2, 1, 2),+-- >      (2, 2, 4)+-- >    ]+-- >    (\a b expected -> a * b == expected)+--+-- > describe "reverse" $+-- >  byExample+-- >    ("list", "reversed")+-- >    [("abc", "cba"), ("", ""), ("0123456", "6543210")]+-- >    (shouldBe . reverse)+module Test.Hspec.Tables+  ( Table (..),+    byExample,+    testTable,+  )+where++import Test.Hspec.Core.Spec++-- | A type class for tables.+--+-- A table type binds together the row type @r@ with the type of its header and the "generic" curried function @forall p. r -> p@+class Table r where+  type Header r+  type Forall r p+  apply :: Forall r p -> r -> p++  showHeader :: Header r -> String+  default showHeader :: Show (Header r) => Header r -> String+  showHeader = show++instance Table (a, b) where+  type Header (a, b) = (String, String)+  type Forall (a, b) p = a -> b -> p+  apply = uncurry++instance Table (a, b, c) where+  type Header (a, b, c) = (String, String, String)+  type Forall (a, b, c) p = a -> b -> c -> p+  apply f (a, b, c) = f a b c++instance Table (a, b, c, d) where+  type Header (a, b, c, d) = (String, String, String, String)+  type Forall (a, b, c, d) p = a -> b -> c -> d -> p+  apply f (a, b, c, d) = f a b c d++instance Table (a, b, c, d, e) where+  type Header (a, b, c, d, e) = (String, String, String, String, String)+  type Forall (a, b, c, d, e) p = a -> b -> c -> d -> e -> p+  apply f (a, b, c, d, e) = f a b c d e++instance Table (a, b, c, d, e, f) where+  type Header (a, b, c, d, e, f) = (String, String, String, String, String, String)+  type Forall (a, b, c, d, e, f) p = a -> b -> c -> d -> e -> f -> p+  apply f (a, b, c, d, e, f_) = f a b c d e f_++instance Table (a, b, c, d, e, f, g) where+  type Header (a, b, c, d, e, f, g) = (String, String, String, String, String, String, String)+  type Forall (a, b, c, d, e, f, g) p = a -> b -> c -> d -> e -> f -> g -> p+  apply f (a, b, c, d, e, f_, g) = f a b c d e f_ g++-- | Creates a 'Spec' from the /table/ consisting of:+--+--    * header+--    * list of examples (/rows/)+--    * assertion+--+--  The resulting spec consists of one test per each /row/.+--  For example:+--+--  > byExample+--  >   ("list", "reversed")+--  >   [("abc", "cba"), ("", ""), ("0123456", "6543210")]+--  >  (shouldBe . reverse)+--+--  is equivalent to:+--+--  > describe (show ("list", "reversed")) $ do+--  >   specify (show ("abc", "cba")) $ reverse "abc" `shouldBe` "cba"+--  >   specify (show ("", "")) $ reverse "" `shouldBe` ""+--  >   specify (show ("0123456", "6543210")) $ reverse "0123456" `shouldBe` "6543210"+byExample ::+  forall a r.+  (Table r, Example a, Show r) =>+  -- | /header/ - tuple of strings (max 7 for now); used to 'describe' this spec, its arity == number of columns+  Header r ->+  -- | /rows/ - list of tuples of examples; arity of each tuple must match the number of columns (== arity of the /header/)+  [r] ->+  -- | /assertion/ - curried function from a row to an @a@ - if /row/ type is @(b,c,d)@ then it must be @('Example' a) => b -> c -> d -> a@+  Forall r a ->+  SpecWith (Arg a)+byExample header table test =+  describe (showHeader @r header) $ mapM_ (\row -> specify @a (show row) $ apply test row) table++-- | Alias for 'byExample'+testTable ::+  forall a r.+  (Table r, Example a, Show r) =>+  Header r ->+  [r] ->+  Forall r a ->+  SpecWith (Arg a)+testTable = byExample @a
+ test/ExampleSpecs.hs view
@@ -0,0 +1,34 @@+module ExampleSpecs (exampleSpecs) where++import Test.Hspec+import Test.Hspec.Tables++exampleSpecs :: Spec+exampleSpecs = describe "Example specs" $ do+  example1+  example2++example1 :: Spec+example1 =+  describe "multiplication table" $+    byExample+      ("x", "y", "result")+      [ (0, 0, 0),+        (0, 1, 0),+        (0, 2, 0),+        (1, 0, 0),+        (1, 1, 1),+        (1, 2, 2),+        (2, 0, 0),+        (2, 1, 2),+        (2, 2, 4)+      ]+      (\a b expected -> a * b == expected)++example2 :: Spec+example2 =+  describe "reverse" $+    byExample+      ("list", "reversed")+      [("abc", "cba"), ("", ""), ("0123456", "6543210")]+      (shouldBe . reverse)
+ test/Spec.hs view
@@ -0,0 +1,95 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE RecordWildCards #-}++import Control.Monad (join)+import Data.Foldable+import Data.Functor ((<&>))+import ExampleSpecs+import Test.Hspec+import Test.Hspec.Core.Runner (ColorMode (..), Config (..), Summary (..), defaultConfig, runSpec)+import Test.Hspec.Core.Spec (Item (..), runSpecM)+import Test.Hspec.Tables++main :: IO ()+main = hspec $ do+  spec+  exampleSpecs++spec :: Spec+spec = forM_ [spec2Bool, spec2BoolF, spec2Expectation, spec3, spec4, spec5, spec6, spec7] specMetaSpec++data MetaSpec b = forall a. Show a => MetaSpec {label :: String, def :: [a], thisSpec :: SpecWith b, expectedFailures :: Int}++withFailures :: MetaSpec a -> Int -> MetaSpec a+withFailures meta failures = meta {expectedFailures = failures}++describeMeta :: (Show a) => String -> [a] -> ([a] -> SpecWith b) -> MetaSpec b+describeMeta label def generateSpec = MetaSpec label def generatedSpec 0+  where+    generatedSpec = generateSpec def++spec2Bool :: MetaSpec ()+spec2Bool = describeMeta+  "spec2Bool"+  [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]+  $ \examples -> byExample ("A", "expected") examples (\a expected -> expected == a * 2)++spec2BoolF :: MetaSpec ()+spec2BoolF =+  describeMeta+    "spec2BoolF"+    [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]+    (\examples -> byExample ("A", "expected") examples (==))+    `withFailures` 4++spec2Expectation :: MetaSpec ()+spec2Expectation = describeMeta+  "spec2Expectation"+  [(0, 0), (1, 2), (2, 4), (3, 6), (4, 8)]+  $ \examples -> byExample ("A", "expected") examples (shouldBe . (* 2))++spec3 :: MetaSpec ()+spec3 = describeMeta+  "spec3"+  [(0, 0, 0), (1, 1, 2), (2, 2, 4), (3, 3, 6), (4, 4, 8), (5, 5, 10)]+  $ \examples -> byExample ("A", "B", "expected") examples (\a b expected -> a + b == expected)++spec4 :: MetaSpec ()+spec4 = describeMeta+  "spec4"+  [('a', 'b', 'c', "abc"), ('g', 'e', 'f', "gef"), ('x', 'y', 'z', "xyz")]+  $ \examples -> byExample ("A", "B", "C", "expected") examples (\a b c expected -> [a, b, c] `shouldBe` expected)++spec5 :: MetaSpec ()+spec5 = describeMeta+  "spec5"+  [('a', 'b', 'c', 'd', "abcd"), ('g', 'e', 'f', 'h', "gefh"), ('x', 'y', 'z', 'a', "xyza")]+  $ \examples -> byExample ("A", "B", "C", "D", "expected") examples (\a b c d expected -> [a, b, c, d] `shouldBe` expected)++spec6 :: MetaSpec ()+spec6 = describeMeta+  "spec6"+  [(1, 1, 1, 1, 1, 1), (2, 2, 2, 2, 2, 32)]+  $ \examples -> byExample ("A", "B", "C", "D", "E", "expected") examples (\a b c d e expected -> a * b * c * d * e == expected)++spec7 :: MetaSpec ()+spec7 = describeMeta+  "spec7"+  [(1, 1, 1, 1, 1, 1, 1), (2, 2, 2, 2, 2, 2, 64)]+  $ \examples -> byExample ("A", "B", "C", "D", "E", "F", "expected") examples (\a b c d e f expected -> a * b * c * d * e * f == expected)++_config :: Config+_config = defaultConfig {configIgnoreConfigFile = True, configColorMode = ColorNever}++specMetaSpec :: MetaSpec () -> Spec+specMetaSpec MetaSpec {..} = describe label $ do+  specify "Analysis" $ expectCorrectSpec def thisSpec+  let expectedSummary = Summary (length def) expectedFailures+  specify "Execution" $ runSpec thisSpec _config `shouldReturn` expectedSummary++expectCorrectSpec :: Show a => [a] -> SpecWith b -> Expectation+expectCorrectSpec def generatedSpec = runSpecM generatedSpec <&> getAllItems >>= (shouldMatchList labels . itemRequirements)+  where+    itemRequirements = map itemRequirement+    getAllItems = join . map toList+    labels = map show def