测试Haskell代码:分离状态
我正在哈斯克尔写一个在线游戏。。 我的游戏引擎位于statemonad中,因此测试起来很简单,我们已经为它编写了属性。这就是幸福 但是,服务器有一个用户列表,它位于测试Haskell代码:分离状态,haskell,testing,Haskell,Testing,我正在哈斯克尔写一个在线游戏。。 我的游戏引擎位于statemonad中,因此测试起来很简单,我们已经为它编写了属性。这就是幸福 但是,服务器有一个用户列表,它位于MVar中。有很多线程在运行。一个游戏线程,tcp和websocket线程,每个客户端都有自己的线程等。所有这些线程都与通道进行对话 问题在于测试一些用户工作流;像 即使在游戏开始之前,用户也会立即加入和退出 用户在游戏中间离开 用户退出游戏等等 用户工作流与IO()中的所有线程耦合。 我尝试通过传递通道(以值作为前缀)对函数编写单元
MVar
中。有很多线程在运行。一个游戏线程,tcp和websocket线程,每个客户端都有自己的线程等。所有这些线程都与通道进行对话
问题在于测试一些用户工作流
;像
IO()
中的所有线程耦合。
我尝试通过传递通道(以值作为前缀)对函数编写单元测试。但它们看起来很脆弱。每次我修改代码,都很难理解为什么测试失败。这不是很糟糕,但我正在寻找更好的方法
如何测试我的工作流?我可以使用其他类型的编程抽象吗
我愿意接受各种各样的建议(FRP/免费单子:我一个都不知道……但如果它们解决了问题,我准备学习。)
相关代码:
data Server = Server { serverGameConfig :: Game.GameConfig
, serverUsers :: MVar (M.Map UserID User)
, networkChans :: NetworkChans
, serverChan :: TChan InMessage
, clientsChan :: Chan OutMessage
, internalChan :: Chan ServerSignals
}
runClient :: UserID -> Chan InMessage -> Server -> IO ()
runClient uId clientChan server@Server{..} = do
let (_, clientSpecificOutChan) = networkChans
writeChan clientSpecificOutChan $ (uId, ServerMsg "Take a nick name : ")
msg <- readChan clientChan
let nick = case msg of
PlayerName _ name -> Just name
_ -> Nothing
case nick of
Nothing -> runClient uId clientChan server
Just _ -> do
let user = User uId nick Waiting
failedToAdd <- modifyMVar serverUsers $ \users ->
-- for player we still have name as id ; so keep it unique.
if isNickTaken users nick
then return (users, True)
else return (M.insert uId user users, False)
if failedToAdd
then runClient uId clientChan server
else do atomically $ writeTChan serverChan $ PlayerJoined uId
writeChan clientSpecificOutChan $ (uId, ServerMsg $ "Hi.. " ++ T.unpack (fromJust nick) ++ ".. Type ready when you are ready to play.. quit to quit.")
fix $ \loop -> do m <- readChan clientChan
case m of
PlayerReady _ -> playClient uId clientChan server
PlayerExit _ -> atomically $ writeTChan serverChan $ PlayerExit uId
_ -> loop
where
isNickTaken users nick = any (\u -> nick == userNick u) users
cleanString :: String -> String
cleanString = reverse . dropWhile (\c -> c == '\n' || c == '\r') . reverse
playClient :: UserID -> Chan InMessage -> Server -> IO ()
playClient clientId inChan Server{..} = do
-- because every client wants same copy of the message, duplicate channel.
outClientChan <- dupChan clientsChan
writeChan outClientChan $ ServerMsg "Waiting for other players to start the game...!!!"
let (_, clientSpecificOutChan) = networkChans
writer <- forkIO $ forever $ do
outMsg <- readChan outClientChan
writeChan clientSpecificOutChan (clientId, outMsg)
clientInternalChan <- dupChan internalChan
-- otherwise gameready msg for this second client joining is lost.
atomically $ writeTChan serverChan $ PlayerReady clientId
-- block on ready-signal from server-thread to start the game.
signal <- readChan clientInternalChan
case signal of
GameReadySignal config players -> writeChan outClientChan $ GameReady config players
writeList2Chan outClientChan [ ServerMsg "Here.. you go!!!"
, ServerMsg "Movements: type L for left , R for right, Q for quit... enjoy." ]
-- todo: for this functionality we would need to use TChan for userChan/inChan
-- hFlush clientHdl
fix $ \loop ->
do
msg <- readChan inChan
case msg of
PlayerExit _ -> void $ writeChan outClientChan $ ServerMsg "Sayonara !!!"
_ -> atomically (writeTChan serverChan msg) >> loop
killThread writer
atomically $ writeTChan serverChan (PlayerExit clientId)
dataserver=Server{serverGameConfig::Game.GameConfig
,serverUsers::MVar(M.Map UserID User)
,网络通道::网络通道
,serverChan::TChan InMessage
,clientsChan::Chan OutMessage
,internalChan::Chan服务器信号
}
runClient::UserID->Chan-InMessage->Server->IO()
运行客户端uId客户端server@Server{..}=do
let(u,clientSpecificOutChan)=网络通道
writeChan clientSpecificOutChan$(uId,ServerMsg“使用昵称:”)
味精只是名字
_->没有
尼克案
Nothing->runClient uId客户端服务器
只要做
让user=user-uId等待
失败加载
--对于玩家,我们仍然有名字作为id;因此,保持它的独特性。
如果是NickTaken用户nick
然后返回(用户,True)
else返回(M.insert uId user,False)
如果加载失败
然后运行客户端uId clientChan服务器
否则执行原子化$writeTChan serverChan$PlayerJoined uId
writeChan clientSpecificOutChan$(uId,ServerMsg$“Hi..”++T.unpack(fromJust nick)++”。准备好播放时键入ready..退出以退出。“)
修复$\loop->do m playClient uId clientChan服务器
PlayerExit->atomically$writeTChan serverChan$PlayerExit uId
_->环路
哪里
isNickTaken users nick=any(\u->nick==userNick u)用户
cleanString::String->String
cleanString=反向。dropWhile(\c->c='\n'| c=='\r')。颠倒
playClient::UserID->Chan-InMessage->Server->IO()
playClient客户端ID在服务器{..}=do中
--因为每个客户端都需要相同的消息副本,所以需要复制通道。
outClientChan循环
死线书写器
原子级$writeTChan serverChan(PlayerExit客户端ID)
如您所见,用户在MVar中。。(TVar并不重要,因为只有一个线程写入)。问题在于测试
如何构造代码,使
runClient
中的workflow logic
和playClient
可以分开进行测试?请在您的问题中包含相关代码,因为您可能会关闭GitHub存储库,从而使您的问题无效。而且,你的问题是开放式的。你几乎要求一个教程,这是离题的。A可以将你的问题缩小到其规模的一小部分(这很容易回答),并且仍然可以为你和未来的读者提供有价值的信息。@Zeta你是对的。。我已经包含了两个函数,现在我正在寻求构造此代码的正确方法。。这样好吗?我想。请注意,如果您的代码有效,您的问题就有资格获得,这适用于包含大量代码的问题。但这是一个旁白。请注意,顺便说一句,您的示例不是我所说的“最小值”。但是,很可能很难用一种简单的方式来说明您的问题。您正在使用IO
来管理状态,这使得测试有状态代码变得困难。您应该使用类似于StateT服务器IO
的内容,而不是IO
。然而,您的问题似乎特别是测试用户交互-困难可能是在准确的时间写入相应的句柄,例如在用户完成连接之前断开连接。如果是这种情况,您应该将用户交互代码抽象为一个typeclass,例如,class UserInteractionMonad m where pollMsg::m Message;sendMsg::Message->m()
。此外,问题“我如何构造代码,使runClient和playClient中的工作流逻辑可以分离?”并不十分清楚,而且似乎与“如何测试我的工作流?”不是同一个问题-你应该将每个问题限制为一个问题。