Skip to content

Latest commit

History

277 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Graphula

HackageStackage NightlyStackage LTSCI

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
|]
instanceArbitrarySchoolwhere
arbitrary = genericArbitrary
instanceArbitraryTeacherwhere
arbitrary = genericArbitrary
instanceArbitraryCoursewhere
arbitrary = genericArbitrary
instanceArbitraryStudentwhere
arbitrary = genericArbitrary
instanceArbitraryQuestionwhere
arbitrary = genericArbitrary
instanceArbitraryAnswerwhere
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.

instanceHasDependenciesSchoolinstanceHasDependenciesStudentinstanceHasDependenciesQuestion

For single-dependency models, we use the Only type.

instanceHasDependenciesTeacherwheretypeDependenciesTeacher=OnlySchoolId

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.

instanceHasDependenciesCoursewheretypeDependenciesCourse= (SchoolId, TeacherId)
instanceHasDependenciesAnswerwheretypeDependenciesAnswer= (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 =doletlogFile::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)

Generation Failures

Generating the graph can fail if you ask it do impossible things, such as generate entities that collide on a unique constraint. In such cases, you will receive an informative error:

generationFailureSpec::IO()
generationFailureSpec =do
result <- try $ runGraphulaT Nothing runDB $do
school <- node @School()mempty-- collision that will never resolve
nodeKeyed @School (entityKey school) ()memptycase result ofLeft ex ->
displayException @GenerationFailure ex
`shouldBe`"GenerationFailureMaxAttemptsToInsert (Just \"entity already exists by this key\") School"Right _ ->pure()

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

Release

To release a new version of this library, push a commit to main using a conventionally-formatted commit message.

  • Prefix with fix: to release a new patch version,
  • Prefix with feat: to release a new minor version, or
  • Prefix with feat!: to release a new major version

To change the "epoch" version, edit it in package.yaml and change the .releaserc.yaml tag prefix to match.

About

A simple interface for generating persistent data and linking its dependencies

Topics

Resources

Stars

48 stars

Watchers

24 watching

Forks

Releases

Used by

Contributors

Languages