Я не уверен в правильном названии для своего вопроса, но, похоже, я несколько загнал себя в угол. Или, по крайней мере, я столкнулся с немного неудобным дизайнерским решением в моем наборе тестов hspe c для моего проекта.
Мой проект должен выполнять некоторые вызовы сторонних API, а в HSpe c я пытаюсь чтобы их можно было подделать с помощью преобразователя, реализованного с использованием StateT
, который можно поместить в тестовый стек, чтобы обеспечить возможность «подделывать» эти вызовы API. Я всегда хочу заглушить эти вызовы в модульных тестах, я никогда не хочу попадать в реальный API.
Я «заглушаю» вызовы, определяя все эффекты моей системы с помощью классов типов и предоставляя другой экземпляр при тестировании. Так, например, у меня был бы такой класс:
class FireApiGetRequest m where
fireApiGetRequest :: GetRequest -> m (Either ApiError GetReponse)
instance FireApiGetRequest Hander where
fireApiGetRequest = -- real world implementation
У меня есть два стека трансформаторов для моих тестов, в настоящее время один известен как AppTestIO
и AppTestPureM
.
AppTestIO
состоит из ReaderT
, который содержит пул соединений с реальной тестовой базой данных и некоторые настройки тестовой конфигурации, а AppTestPureM
- это псевдоним типа для StateT FakeDatabaseCalls Gen
, почти так же, как я записываю вызовы API с классы типов, вызовы базы данных также разделяются на классы типов и таким же образом «заглушаются». Это работает хорошо, только я хотел бы добавить в оба стека слой-преобразователь, который имитирует сторонний API. На мой взгляд, я должен быть в состоянии определить трансформатор, который может находиться в обоих этих стеках трансформаторов и дать мне возможность имитировать эти вызовы API:
type AppTestPureM = StateT FakedDatabaseCalls (ThirdPartyApiMocksT Gen)
type AppTestIO = ThirdPartyApiMocksT (AppTestT IO)
newtype AppTestT m a = AppTestT
{ unAppTestT :: ReaderT TestEnv m a
}
На первый взгляд это кажется отличным идея, потому что это означает, что я могу заглушить эти вызовы API независимо от того, попадает ли тест в реальную базу данных, в основном.
Для экземпляров у меня нет проблем с определением экземпляров для вызовов API в тесте:
instance FireApiGetRequest ThirdPartyApiMocksT where
fireApiGetRequest = -- test implementation, access state and return value based on that
instance FireApiGetRequest m => FireApiGetRequest (StateT s m) where
fireApiGetRequest = lift . fireApiGetRequest
И я могу определить вспомогательные функции в моем тесте, чтобы заставить экземпляры возвращать фальшивые данные, которые мне нужны:
stubApiGetRequest :: Monad m => (Either ApiError GetResponse) -> ThirdPartyApiMocksT m ()
stubApiGetRequest returnVal = undefined -- store `returnVal` in the state for use in typeclass instance
Моя проблема возникает, когда я действительно начинаю использовать эти экземпляры внутри мои тесты. Для тех функций в моем приложении, которые попадают в базу данных (которые еще не заглушены экземплярами классов типов), они в конечном итоге используют runSqlPool
, в котором используется MonadUnliftIO
, а MonadUnliftIO
фактически запрещает мне смешивать их вместе.
Подход к достижению «состояния» при использовании MonadUnliftIO
заключается в использовании вместо него ReaderT
+ MVar
. Обычно у меня нет большой проблемы с этим, моя единственная проблема здесь в том, что мой стек PureM
основан на StateT
, а также не работает в IO
, потому что он используется для выполнения `` эффективных '' вычислений использование поддельных данных для написания теста в чистом виде, без ввода-вывода. Это также означает, что я могу использовать quickcheck
для проверки этих функций, если я хочу это сделать. Я знаю, что существует quickcheck-monadic
, поэтому наличие этих функций в результате IO
не может быть концом света, но я хотел бы сохранить AppTestPureM a -> Gen a
. Использование ReaderT
+ MVar
здесь в этой ситуации приведет к тому, что эти тесты будут зависеть от IO
. Итак, эти два набора тестов противоречат друг другу.
Полагаю, это иллюстрирует ситуацию, в которой я сейчас нахожусь. Я не знаю, как именно действовать дальше.