newlandsvalley / tunebank

RESTful server for tunes represented in the ABC notation
BSD 3-Clause "New" or "Revised" License
0 stars 0 forks source link

Work out how best to mock the DB layer #14

Closed newlandsvalley closed 4 years ago

newlandsvalley commented 4 years ago

This is very usefully discussed in some depth in https://functor.tokyo/blog/2015-11-20-testing-db-access. I decided to look at the typeclass option first - as being the most likely contender.

The basic idea here is to abstract away the actual DB access by defining a very abstract typeclass. This has different instances in the production and test environments. In the former we use an actual DB connection and DB monad. In the latter we mock this by means of a 'connection' to Reader state held inside an IORef and by a pseudo DB monad which in fact just accesses test state via a ReaderT monad.

This sounds OK but is definitely not trivial. It is invasive in the server code because of the highly abstract types. This uses functional dependencies in order to help the Haskell compiler unify the types. It uses a whole raft of pragmas - including FlexibleInstances, FlexibleContexts, GeneralizedNewtypeDeriving, RankNTypes, ScopedTypeVariables, TypeOperators, FunctionalDependencies and InstanceSigs.

newlandsvalley commented 4 years ago

This is explored in the mock branch.

newlandsvalley commented 4 years ago

But the exploration is getting nowhere. What little I know about mtl has been learnt from PureScript. There, for example, if the base monad in your stack is Aff, you can generalise this by moving to MonadAff m. This then allows you to add further constraints along the lines of:

someFunc :: DBAccess m => ..... -> MonadAff m

which would allow your mtl stack to use database access.

However, with Servant, the base monad is handler but there is no accompanying HandlerMonad. So it seems to me we can't add an abstract DB interface in this way.

I imagine that this is the reason for the functional dependencies described above. If you supply a concrete DB connection, the compiler can then solve the DBAccess constraint. This does not seem to be an option for me either, because it requires the individual database requests to return some sort of Query type which can then be executed by the runDb function which, in the tutorial above, is executed in Persistent. This is not really an appropriate model with postgresql-simple.

newlandsvalley commented 4 years ago

Well, commit aa7dc830a002e3e702fe4cbd0094b2d4b6d15f5e has at least got to the point where there are separate implementations of the database API for production and test. I am not happy with it though - it seems to me to be a subterfuge. For any particular database query named foo you have to invoke runquery dbConfig $ foo.

What happens is that the query itself runs in IO. runQuery runs the reader monad to install the config and evokes liftIO on foo to lift it into the database monad. foo simply reads the config and executes the query.

Because there is a functional dependency between the config and the database monad, the type checker appears to be satisfied. It seems a very roundabout way of doing things.

newlandsvalley commented 4 years ago

OK - I still have reservations but mocking is now complete. The mock branch has been folded in to master,