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 +11/−0
- LICENSE +21/−0
- README.md +99/−0
- hspec-tables.cabal +61/−0
- src/Test/Hspec/Tables.hs +129/−0
- test/ExampleSpecs.hs +34/−0
- test/Spec.hs +95/−0
+ 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++[](https://hackage.haskell.org/package/hspec-tables)+[](http://stackage.org/lts/package/hspec-tables)+[](http://stackage.org/nightly/package/hspec-tables)+[](LICENSE)+[](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