Graphula
Graphula is a simple interface for generating persistent data and linking its
dependencies. We use this interface to generate fixtures for automated testing.
Arbitrary Data
Graphula utilizes QuickCheck
to generate random data. We need to declare
Arbitrary
instances for our models.
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
School
name String
deriving Show Eq Generic
Teacher
schoolId SchoolId
name String
deriving Show Eq Generic
Course
schoolId SchoolId
teacherId TeacherId
name String
deriving Show Eq Generic
Student
name String
deriving Show Eq Generic
Question
content String
deriving Show Eq Generic
Answer
questionId QuestionId
studentId StudentId
yes Bool
UniqueAnswer questionId studentId
deriving Show Eq Generic
|]
instance Arbitrary School where
arbitrary = genericArbitrary
instance Arbitrary Teacher where
arbitrary = genericArbitrary
instance Arbitrary Course where
arbitrary = genericArbitrary
instance Arbitrary Student where
arbitrary = genericArbitrary
instance Arbitrary Question where
arbitrary = genericArbitrary
instance Arbitrary Answer where
arbitrary = genericArbitrary
Dependencies
We declare dependencies via the HasDependencies
typeclass and its associated
type Dependencies
. If a model does not have any dependencies, we only need to
declare an empty instance.
instance HasDependencies School
instance HasDependencies Student
instance HasDependencies Question
For single-dependency models, we use the Only
type.
instance HasDependencies Teacher where
type Dependencies Teacher = Only SchoolId
Multi-dependency models use tuples. Declare these dependencies in the order they
appear in the model's type definition. HasDependencies
leverages generic
programming to inject dependencies for you.
instance HasDependencies Course where
type Dependencies Course = (SchoolId, TeacherId)
instance HasDependencies Answer where
type Dependencies Answer = (QuestionId, StudentId)
Logging failures
runGraphulaLogged
will dump generated data to a temporary file. Or
runGraphulaLoggedWithFileT
can be used to pass an explicit path.
loggingSpec :: IO ()
loggingSpec = do
let
logFile :: FilePath
logFile = "test.graphula"
failingGraph :: IO ()
failingGraph = runGraphulaT Nothing runDB . runGraphulaLoggedWithFileT logFile $ do
student <- node @Student () mempty
question <- node @Question () mempty
answer <- node @Answer
(entityKey question, entityKey student)
$ edit $ \a -> a { answerYes = True }
-- Test failures will cause the graph to be logged (not any exception)
liftIO $ answerYes (entityVal answer) `shouldBe` False
failingGraph `shouldThrow` anyException
n <- lines <$> readFile logFile
n `shouldSatisfy` (not . null)
Running It
simpleSpec :: IO ()
simpleSpec =
runGraphulaT Nothing runDB $ do
school <- node @School () mempty
teacher <- node @Teacher (Only $ entityKey school) mempty
course <- node @Course (entityKey school, entityKey teacher) mempty
student <- node @Student () $ edit $ \s -> s { studentName = "Pat" }
question <- node @Question () mempty
answer <- node @Answer
(entityKey question, entityKey student)
$ edit $ \a -> a { answerYes = True }
liftIO $ do
-- Typically, you would run some other function like "fetch correct
-- answers at school" and assert you found the correct answers you
-- generated. In this example we just assert some things about the data
-- directly:
teacherSchoolId (entityVal teacher) `shouldBe` entityKey school
courseTeacherId (entityVal course) `shouldBe` entityKey teacher
answerYes (entityVal answer) `shouldBe` True