hspec-expectations-json 1.0.0.7 → 1.0.2.0
raw patch · 10 files changed
+323/−120 lines, 10 filesdep +QuickCheckdep ~Diffdep ~HUnitdep ~aesonPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck
Dependency ranges changed: Diff, HUnit, aeson, aeson-pretty, aeson-qq, hspec, scientific, text, unordered-containers, vector
API changes (from Hackage documentation)
+ Test.Hspec.Expectations.Json: data Normalizer
+ Test.Hspec.Expectations.Json: defaultNormalizer :: Normalizer
+ Test.Hspec.Expectations.Json: expandHeterogenousArrays :: Normalizer
+ Test.Hspec.Expectations.Json: ignoreArrayOrdering :: Normalizer
+ Test.Hspec.Expectations.Json: instance GHC.Base.Functor Test.Hspec.Expectations.Json.Actual
+ Test.Hspec.Expectations.Json: instance GHC.Base.Functor Test.Hspec.Expectations.Json.Expected
+ Test.Hspec.Expectations.Json: instance GHC.Base.Monoid Test.Hspec.Expectations.Json.Normalizer
+ Test.Hspec.Expectations.Json: instance GHC.Base.Semigroup Test.Hspec.Expectations.Json.Normalizer
+ Test.Hspec.Expectations.Json: shouldBeJsonNormalized :: HasCallStack => Normalizer -> Value -> Value -> IO ()
+ Test.Hspec.Expectations.Json: subsetActualToExpected :: Normalizer
+ Test.Hspec.Expectations.Json: treatNullsAsMissing :: Normalizer
+ Test.Hspec.Expectations.Json.Internal: expandHeterogenousArrays :: Value -> Value
+ Test.Hspec.Expectations.Json.Internal: filterNullFields :: Value -> Value
+ Test.Hspec.Expectations.Json.Lifted: data Normalizer
+ Test.Hspec.Expectations.Json.Lifted: defaultNormalizer :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: expandHeterogenousArrays :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: ignoreArrayOrdering :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: shouldBeJsonNormalized :: (HasCallStack, MonadIO m) => Normalizer -> Value -> Value -> m ()
+ Test.Hspec.Expectations.Json.Lifted: subsetActualToExpected :: Normalizer
+ Test.Hspec.Expectations.Json.Lifted: treatNullsAsMissing :: Normalizer
Files
- CHANGELOG.md +9/−0
- README.md +5/−0
- hspec-expectations-json.cabal +14/−12
- library/Test/Hspec/Expectations/Json.hs +78/−21
- library/Test/Hspec/Expectations/Json/Color.hs +8/−6
- library/Test/Hspec/Expectations/Json/Internal.hs +59/−30
- library/Test/Hspec/Expectations/Json/Lifted.hs +18/−3
- tests/Spec.hs +1/−1
- tests/Test/Hspec/Expectations/Json/InternalSpec.hs +44/−8
- tests/Test/Hspec/Expectations/JsonSpec.hs +87/−39
CHANGELOG.md view
@@ -1,5 +1,14 @@ ## [_Unreleased_](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.0...main) +## [v1.0.2.0](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.1...v1.0.2.0)++- Add `shouldBeJsonNormalized` and `Normalizer` to better support configurable matching+- Added new option for `treatNullsAsMissing` that will treat nulls fields as if they are the same as omitted ones when doing a comparison++## [v1.0.1.1](https://github.com/freckle/hspec-expectations-json/compare/v1.0.1.0...v1.0.1.1)++- Add invariant for all matchers for equality. (ex: forall a. a `shouldMatchJson` a)+ ## [v1.0.1.0](https://github.com/freckle/hspec-expectations-json/compare/v1.0.0.6...v1.0.1.0) - Colorize diff in expectation failure, if connected to a terminal or running on
README.md view
@@ -1,5 +1,10 @@ # Hspec Expectations for JSON Values +[](https://hackage.haskell.org/package/hspec-expectations-json)+[](http://stackage.org/nightly/package/hspec-expectations-json)+[](http://stackage.org/lts/package/hspec-expectations-json)+[](https://github.com/freckle/hspec-expectations-json/actions/workflows/ci.yml)+ Comparing JSON `Value`s in Haskell tests comes with some challenges: - In API responses, additive changes are typically safe and an important way to
hspec-expectations-json.cabal view
@@ -1,6 +1,6 @@ cabal-version: 1.18 name: hspec-expectations-json-version: 1.0.0.7+version: 1.0.2.0 license: MIT license-file: LICENSE copyright: 2020 Freckle Education@@ -59,15 +59,15 @@ TypeApplications TypeFamilies build-depends:- Diff >=0.3.4,- HUnit >=1.6.0.0,- aeson >=1.3.1.1,- aeson-pretty >=0.8.7,+ Diff,+ HUnit,+ aeson,+ aeson-pretty, base >=4.11 && <5,- scientific >=0.3.6.2,- text >=1.2.3.1,- unordered-containers >=0.2.9.0,- vector >=0.12.0.2+ scientific,+ text,+ unordered-containers,+ vector test-suite spec type: exitcode-stdio-1.0@@ -89,7 +89,9 @@ TypeApplications TypeFamilies build-depends:- aeson-qq >=0.8.2,+ QuickCheck,+ aeson,+ aeson-qq, base >=4.11 && <5,- hspec >=2.5.5,- hspec-expectations-json -any+ hspec,+ hspec-expectations-json
library/Test/Hspec/Expectations/Json.hs view
@@ -13,27 +13,49 @@ -- +--------------------------+-------------------+-------------------+ -- | 'shouldMatchOrderedJson' | No | Yes | -- +--------------------------+-------------------+-------------------+--- module Test.Hspec.Expectations.Json- ( shouldBeJson+ ( shouldMatchJson+ , shouldBeJson+ , shouldBeJsonNormalized+ , Normalizer+ , defaultNormalizer+ , treatNullsAsMissing+ , ignoreArrayOrdering+ , subsetActualToExpected+ , expandHeterogenousArrays++ -- * Legacy API++ -- | Prefer to use shouldBeJsonNormalized with the appropriate 'Normalizer' , shouldBeUnorderedJson- , shouldMatchJson , shouldMatchOrderedJson - -- * As predicates- -- | These are only created when a specific need arises+ -- * As predicates++ -- | These are only created when a specific need arises , matchesJson- )-where+ ) where import Prelude +import Control.Monad (unless) import Data.Aeson import Data.Aeson.Encode.Pretty (encodePretty)+import Data.Bifunctor+import Data.Semigroup (Endo (..)) import Data.Text.Lazy (toStrict) import Data.Text.Lazy.Encoding (decodeUtf8) import GHC.Stack import Test.Hspec.Expectations.Json.Internal+ ( Subset (..)+ , Superset (..)+ , assertBoolWithDiff+ , filterNullFields+ , normalizeScientific+ , pruneJson+ , sortJsonArrays+ )+import qualified Test.Hspec.Expectations.Json.Internal as Internal -- $setup -- >>> :set -XQuasiQuotes@@ -43,6 +65,48 @@ -- >>> let printFailure (HUnitFailure _ r) = putStr $ formatFailureReason r -- >>> let catchFailure f = handle printFailure $ f >> putStrLn "<passed>" +newtype Actual a = Actual a+ deriving (Functor)++newtype Expected a = Expected a+ deriving (Functor)++newtype Normalizer = Normalizer+ { normalize :: Endo (Actual Value, Expected Value)+ }+ deriving newtype (Semigroup, Monoid)++normalizeBoth :: (Value -> Value) -> Normalizer+normalizeBoth f = Normalizer $ Endo $ bimap (fmap f) (fmap f)++treatNullsAsMissing :: Normalizer+treatNullsAsMissing = normalizeBoth filterNullFields++ignoreArrayOrdering :: Normalizer+ignoreArrayOrdering = normalizeBoth sortJsonArrays++expandHeterogenousArrays :: Normalizer+expandHeterogenousArrays = normalizeBoth Internal.expandHeterogenousArrays++subsetActualToExpected :: Normalizer+subsetActualToExpected = Normalizer $ Endo go+ where+ go (Actual a, Expected b) =+ let a' = pruneJson (Superset a) (Subset b)+ in (Actual a', Expected b)++defaultNormalizer :: Normalizer+defaultNormalizer =+ ignoreArrayOrdering <> subsetActualToExpected++shouldBeJsonNormalized :: HasCallStack => Normalizer -> Value -> Value -> IO ()+shouldBeJsonNormalized normalizer a b =+ unless (a == b) $+ assertBoolWithDiff (a' == b') (toText b) (toText a)+ where+ toText = toStrict . decodeUtf8 . encodePretty . normalizeScientific+ (Actual a', Expected b') = appEndo (normalize normalizer) (Actual a, Expected b)+ -- | Compare two JSON values, with a useful diff -- -- >>> :{@@ -62,10 +126,8 @@ -- --- "b": true -- +++ "b": false -- }--- shouldBeJson :: HasCallStack => Value -> Value -> IO ()-shouldBeJson a b = assertBoolWithDiff (a == b) (toText b) (toText a)- where toText = toStrict . decodeUtf8 . encodePretty . normalizeScientific+shouldBeJson = shouldBeJsonNormalized mempty infix 1 `shouldBeJson` @@ -92,9 +154,8 @@ -- +++ "b": false, -- +++ "c": true -- }--- shouldBeUnorderedJson :: HasCallStack => Value -> Value -> IO ()-shouldBeUnorderedJson a b = sortJsonArrays a `shouldBeJson` sortJsonArrays b+shouldBeUnorderedJson = shouldBeJsonNormalized ignoreArrayOrdering infix 1 `shouldBeUnorderedJson` @@ -120,18 +181,16 @@ -- --- "b": true -- +++ "b": false -- }--- shouldMatchJson :: HasCallStack => Value -> Value -> IO ()-shouldMatchJson sup sub =- sortJsonArrays (pruneJson (Superset sup) (Subset sub))- `shouldBeJson` sortJsonArrays sub+shouldMatchJson = shouldBeJsonNormalized defaultNormalizer infix 1 `shouldMatchJson` -- | Compare JSON values with the same semantics as 'shouldMatchJson' matchesJson :: Value -> Value -> Bool-matchesJson sup sub =- sortJsonArrays (pruneJson (Superset sup) (Subset sub)) == sortJsonArrays sub+matchesJson sup sub = sup == sub || sup' == sub'+ where+ (Actual sup', Expected sub') = appEndo (normalize defaultNormalizer) (Actual sup, Expected sub) -- | 'shouldBeJson', ignoring extra Object keys --@@ -157,9 +216,7 @@ -- --- "b": true -- +++ "b": false -- }--- shouldMatchOrderedJson :: HasCallStack => Value -> Value -> IO ()-shouldMatchOrderedJson sup sub =- pruneJson (Superset sup) (Subset sub) `shouldBeJson` sub+shouldMatchOrderedJson = shouldBeJsonNormalized subsetActualToExpected infix 1 `shouldMatchOrderedJson`
library/Test/Hspec/Expectations/Json/Color.hs view
@@ -1,11 +1,11 @@ module Test.Hspec.Expectations.Json.Color- ( Color(..)+ ( Color (..) , getColorize ) where import Prelude -import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.IO.Class (MonadIO (..)) import System.Environment (lookupEnv) import System.IO (hIsTerminalDevice, stdout) @@ -18,10 +18,12 @@ shouldColorize <- liftIO $ (||) <$> isGitHubActions <*> hIsTerminalDevice stdout - pure $ if shouldColorize- then \c x -> escape Reset <> escape c <> x <> escape Reset- else \_ x -> x- where isGitHubActions = (== Just "true") <$> lookupEnv "GITHUB_ACTIONS"+ pure $+ if shouldColorize+ then \c x -> escape Reset <> escape c <> x <> escape Reset+ else \_ x -> x+ where+ isGitHubActions = (== Just "true") <$> lookupEnv "GITHUB_ACTIONS" escape :: Color -> String escape = \case
library/Test/Hspec/Expectations/Json/Internal.hs view
@@ -2,28 +2,28 @@ -- | Internal building-blocks for JSON 'Value' expectations module Test.Hspec.Expectations.Json.Internal- (- -- * Pretty diff+ ( -- * Pretty diff assertBoolWithDiff - -- * Pruning 'Object's- , Superset(..)- , Subset(..)+ -- * Pruning 'Object's+ , Superset (..)+ , Subset (..) , pruneJson - -- * Sorting 'Array's- , Sortable(..)+ -- * Sorting 'Array's+ , Sortable (..) , sortJsonArrays , vectorSortOn - -- * Dealing with 'Scientific'+ -- * Dealing with 'Scientific' , normalizeScientific+ , filterNullFields+ , expandHeterogenousArrays ) where import Prelude - import Data.Aeson #if MIN_VERSION_Diff(0,4,0) import Data.Algorithm.Diff (PolyDiff(..), getDiff)@@ -51,9 +51,10 @@ assertBoolWithDiff :: HasCallStack => Bool -> Text -> Text -> IO () assertBoolWithDiff asserting expected got = do colorize <- getColorize- flip HUnit.assertBool asserting . unlines . map (addSign colorize) $ getDiff- (lines (T.unpack expected))- (lines (T.unpack got))+ flip HUnit.assertBool asserting . unlines . map (addSign colorize) $+ getDiff+ (lines (T.unpack expected))+ (lines (T.unpack got)) where addSign colorize = \case Both _ s -> colorize Reset $ " " ++ s@@ -67,9 +68,9 @@ -- | Recursively remove items in the 'Superset' value not present in 'Subset' pruneJson :: Superset -> Subset -> Value pruneJson (Superset sup) (Subset sub) = case (sup, sub) of- (Object a, Object b) -> Object- $ KeyMap.intersectionWith (\x y -> pruneJson (Superset x) (Subset y)) a b-+ (Object a, Object b) ->+ Object $+ KeyMap.intersectionWith (\x y -> pruneJson (Superset x) (Subset y)) a b -- Pruning elements in Arrays is *extremely* tricky in that it interacts with -- both sorting and matching in what should be a function independent of those -- concerns. There are no good options here, so we make some concessions:@@ -95,11 +96,24 @@ (Array a, Array b) -> Array $ case b V.!? 0 of Nothing -> a Just y -> (\x -> pruneJson (Superset x) (Subset y)) <$> a- (x, _) -> x +-- | Expand objects in arrays to have null values for omitted fields+--+-- ex: [{a:1}, {b:1}] -> [{a:1, b:null}, {a:null, b:1}]+expandHeterogenousArrays :: Value -> Value+expandHeterogenousArrays = go KeyMap.empty+ where+ collectAllKeys = \case+ Object km -> Null <$ km+ _ -> KeyMap.empty+ go allKeys = \case+ Object km -> Object $ expandHeterogenousArrays <$> KeyMap.union km allKeys+ Array vec -> Array $ go (foldMap collectAllKeys vec) <$> vec+ x -> x+ newtype Sortable = Sortable Value- deriving newtype Eq+ deriving newtype (Eq) instance Ord Sortable where Sortable a `compare` Sortable b = case (a, b) of@@ -114,25 +128,26 @@ where arbitraryRank :: Value -> Int arbitraryRank = \case- Object{} -> 5- Array{} -> 4- String{} -> 3- Number{} -> 2- Bool{} -> 1+ Object {} -> 5+ Array {} -> 4+ String {} -> 3+ Number {} -> 2+ Bool {} -> 1 Null -> 0 sortJsonArrays :: Value -> Value sortJsonArrays = \case Array v -> Array $ vectorSortOn Sortable $ sortJsonArrays <$> v Object hm -> Object $ sortJsonArrays <$> hm- x@String{} -> x- x@Number{} -> x- x@Bool{} -> x- x@Null{} -> x+ x@String {} -> x+ x@Number {} -> x+ x@Bool {} -> x+ x@Null {} -> x vectorSortOn :: Ord b => (a -> b) -> Vector a -> Vector a vectorSortOn f v = v V.// zip [0 ..] sorted- where sorted = sortOn f $ V.toList v+ where+ sorted = sortOn f $ V.toList v -- | Normalize all 'Number' values to 'Double' precision --@@ -143,13 +158,27 @@ -- -- This sends them through an 'id' function as 'Double', which will make either -- print as @1.0@ consistently.--- normalizeScientific :: Value -> Value normalizeScientific = \case Object hm -> Object $ normalizeScientific <$> hm Array vs -> Array $ normalizeScientific <$> vs- x@String{} -> x+ x@String {} -> x Number sci -> Number $ Scientific.fromFloatDigits @Double $ Scientific.toRealFloat sci- x@Bool{} -> x+ x@Bool {} -> x x@Null -> x++filterNullFields :: Value -> Value+filterNullFields = go+ where+ go :: Value -> Value+ go = \case+ Object km -> Object $ KeyMap.mapMaybe objectFilter km+ Array vec -> Array $ filterNullFields <$> vec+ x -> x+ objectFilter :: Value -> Maybe Value+ objectFilter = \case+ Object km -> Just $ Object $ KeyMap.mapMaybe objectFilter km+ Array vec -> Just $ Array $ filterNullFields <$> vec+ Null -> Nothing+ x -> Just x
library/Test/Hspec/Expectations/Json/Lifted.hs view
@@ -1,16 +1,31 @@ module Test.Hspec.Expectations.Json.Lifted- ( shouldBeJson+ ( shouldMatchJson+ , shouldBeJson+ , shouldBeJsonNormalized+ , E.Normalizer+ , E.defaultNormalizer+ , E.treatNullsAsMissing+ , E.ignoreArrayOrdering+ , E.subsetActualToExpected+ , E.expandHeterogenousArrays++ -- * Legacy API++ -- | Prefer to use shouldBeJsonNormalized with the appropriate 'Normalizer' , shouldBeUnorderedJson- , shouldMatchJson , shouldMatchOrderedJson ) where import Prelude -import Control.Monad.IO.Class (MonadIO(..))+import Control.Monad.IO.Class (MonadIO (..)) import Data.Aeson import GHC.Stack import qualified Test.Hspec.Expectations.Json as E++shouldBeJsonNormalized+ :: (HasCallStack, MonadIO m) => E.Normalizer -> Value -> Value -> m ()+shouldBeJsonNormalized n x = liftIO . E.shouldBeJsonNormalized n x shouldBeJson :: (HasCallStack, MonadIO m) => Value -> Value -> m () shouldBeJson x = liftIO . E.shouldBeJson x
tests/Spec.hs view
@@ -1,2 +1,2 @@-{-# OPTIONS_GHC -fno-warn-missing-export-lists #-} {-# OPTIONS_GHC -F -pgmF hspec-discover #-}+{-# OPTIONS_GHC -fno-warn-missing-export-lists #-}
tests/Test/Hspec/Expectations/Json/InternalSpec.hs view
@@ -24,13 +24,17 @@ it "prunes object keys recursively Superset not present in Subset" $ do let- sup = Superset [aesonQQ|+ sup =+ Superset+ [aesonQQ| { "foo": { "bar": "bar" , "baz": "baz" } , "bat": "bat" } |] - sub = Subset [aesonQQ|+ sub =+ Subset+ [aesonQQ| { "foo": { "bar": "baz" } } |]@@ -39,19 +43,24 @@ it "prunes objects within Arrays" $ do let- sup = Superset [aesonQQ|+ sup =+ Superset+ [aesonQQ| [ { "foo": "bar", "quix": "cats" } , { "foo": "bats", "quix": "baz" } ] |] - sub = Subset [aesonQQ|+ sub =+ Subset+ [aesonQQ| [ { "foo": "zap" } , { "foo": "zop" } ] |] - pruneJson sup sub `shouldBeJson` [aesonQQ|+ pruneJson sup sub+ `shouldBeJson` [aesonQQ| [ { "foo": "bar" } , { "foo": "bats" } ]@@ -59,19 +68,24 @@ it "handles mismatching types" $ do let- sup = Superset [aesonQQ|+ sup =+ Superset+ [aesonQQ| { "foo": { "bar": 1, "baz": "baz" } , "bat": { "bat": 1 } } |] - sub = Subset [aesonQQ|+ sub =+ Subset+ [aesonQQ| { "foo": { "bar": "baz" } , "bat": "bat" } |] - pruneJson sup sub `shouldBeJson` [aesonQQ|+ pruneJson sup sub+ `shouldBeJson` [aesonQQ| { "foo": { "bar": 1 } , "bat": { "bat": 1 } }@@ -98,3 +112,25 @@ sorted = [aesonQQ|[{ "x": ["number_basics", "number_facts"] }]|] sortJsonArrays unsorted `shouldBeJson` sorted++ describe "filterNullFields" $ do+ it "filters objects" $ do+ let+ withNull = [aesonQQ|{ "x": "x", "y": null }|]+ withoutNull = [aesonQQ|{ "x": "x" }|]++ filterNullFields withNull `shouldBeJson` withoutNull++ it "filters in arrays" $ do+ let+ withNull = [aesonQQ|[{ "x": "x", "y": null }]|]+ withoutNull = [aesonQQ|[{ "x": "x" }]|]++ filterNullFields withNull `shouldBeJson` withoutNull++ it "filters deeply" $ do+ let+ withNull = [aesonQQ|{ "x": {"y": [{"z":"z", "a":null}, {"a": "a"}], "z": null }}|]+ withoutNull = [aesonQQ|{ "x": {"y": [{"z":"z"}, {"a": "a"}]}}|]++ filterNullFields withNull `shouldBeJson` withoutNull
tests/Test/Hspec/Expectations/JsonSpec.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE QuasiQuotes #-} module Test.Hspec.Expectations.JsonSpec@@ -9,10 +10,32 @@ import Data.Aeson.QQ import Test.Hspec import Test.Hspec.Expectations.Json+#if MIN_VERSION_aeson(2,0,3)+import Test.Hspec.QuickCheck (prop)+#endif +matchesJsonTest :: SpecWith ()+#if MIN_VERSION_aeson(2,0,3)+matchesJsonTest = describe "matchesJson" $ do+ prop "always matches itself" $ \a -> a `matchesJson` a+#else+matchesJsonTest = pure ()+#endif+ spec :: Spec spec = do+ matchesJsonTest+ describe "shouldMatchJson" $ do+ it "matches itself" $ do+ let+ a = [aesonQQ|[{id:1}, {id:2, a:"a"}]|]+ b = [aesonQQ|[{id:1, a:"a"}, {id:2}]|]+ c = [aesonQQ|[{id:1, a:"a"}, {id:2, a:"a"}]|]+ a `shouldMatchJson` a+ b `shouldMatchJson` b+ c `shouldMatchJson` c+ it "passes regardless of array order" $ do let a = [aesonQQ|[{ "foo": 1 }, { "foo": 0 }]|]@@ -52,7 +75,8 @@ sup4 = [aesonQQ|{ "studentId": 4, "x": true }|] sub4 = [aesonQQ|{ "studentId": 4 }|] - a = [aesonQQ|+ a =+ [aesonQQ| [ { "stats": [#{sup3}] } , { "stats": [#{sup3}, #{sup1}] } , { "stats": [#{sup4}, #{sup2}] }@@ -62,7 +86,8 @@ ] |] - b = [aesonQQ|+ b =+ [aesonQQ| [ { "stats": [#{sub1}, #{sub3}] } , { "stats": [#{sub3}] } , { "stats": [#{sub2}, #{sub4}] }@@ -76,7 +101,8 @@ it "handles cases where sorting differs after pruning" $ do let- a = [aesonQQ|+ a =+ [aesonQQ| [ { "shortName": "B" , "subSkills": [ { "shortName": "1"@@ -96,7 +122,8 @@ } ] |]- b = [aesonQQ|+ b =+ [aesonQQ| [ { "subSkills": [ { "uspId": "71839723561ba1a49cf2a789dbe50302" } , { "uspId": "4096d2cfebcab73438971ae2304544ee" }@@ -111,39 +138,60 @@ a `shouldMatchJson` b - -- it "is an example failure, to checking how they're printed" $ do- -- let- -- a = [aesonQQ|- -- [ { "shortName": "B"- -- , "subSkills":- -- [ { "shortName": "1"- -- , "uspId": "68fa5xxxxxxxx332f7c88884e5c40e"- -- }- -- ]- -- }- -- , { "shortName": "A"- -- , "subSkills":- -- [ { "shortName": "a"- -- , "uspId": "71839723561ba1a49cf2a789dbe50302"- -- }- -- , { "shortName": "b"- -- , "uspId": "4096d2cfebcab73438971ae2304544ee"- -- }- -- ]- -- }- -- ]- -- |]- -- b = [aesonQQ|- -- [ { "subSkills":- -- [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }- -- , { "uspId": "4096d2cfebcab73438971ae2304544ee" }- -- ]- -- }- -- , { "subSkills":- -- [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }- -- ]- -- }- -- ]- -- |]+ let shouldMatchJsonWithOmittedNullFields =+ shouldBeJsonNormalized $+ treatNullsAsMissing+ <> ignoreArrayOrdering+ <> subsetActualToExpected+ <> expandHeterogenousArrays - -- a `shouldMatchJson` b+ it "ignores omitted null fields" $ do+ let+ a = [aesonQQ|{ "foo": 1 }|]+ b = [aesonQQ|{ "foo": 1, "bar": null }|]++ a `shouldMatchJsonWithOmittedNullFields` b++ it "ignores omitted null fields in arrays" $ do+ let+ a = [aesonQQ|[{ "bar": 1 }, { "foo": 1 }]|]+ b = [aesonQQ|[{ "foo": 1, "bar": null }, { "foo":null, "bar": 1 }]|]++ a `shouldMatchJsonWithOmittedNullFields` b++-- it "is an example failure, to checking how they're printed" $ do+-- let+-- a = [aesonQQ|+-- [ { "shortName": "B"+-- , "subSkills":+-- [ { "shortName": "1"+-- , "uspId": "68fa5xxxxxxxx332f7c88884e5c40e"+-- }+-- ]+-- }+-- , { "shortName": "A"+-- , "subSkills":+-- [ { "shortName": "a"+-- , "uspId": "71839723561ba1a49cf2a789dbe50302"+-- }+-- , { "shortName": "b"+-- , "uspId": "4096d2cfebcab73438971ae2304544ee"+-- }+-- ]+-- }+-- ]+-- |]+-- b = [aesonQQ|+-- [ { "subSkills":+-- [ { "uspId": "71839723561ba1a49cf2a789dbe50302" }+-- , { "uspId": "4096d2cfebcab73438971ae2304544ee" }+-- ]+-- }+-- , { "subSkills":+-- [ { "uspId": "68fa57ddbc1e9aa332f7c88884e5c40e" }+-- ]+-- }+-- ]+-- |]++-- a `shouldMatchJson` b