测试Haskell代码:分离状态

测试Haskell代码:分离状态,haskell,testing,Haskell,Testing,我正在哈斯克尔写一个在线游戏。。 我的游戏引擎位于statemonad中,因此测试起来很简单,我们已经为它编写了属性。这就是幸福 但是,服务器有一个用户列表,它位于MVar中。有很多线程在运行。一个游戏线程,tcp和websocket线程,每个客户端都有自己的线程等。所有这些线程都与通道进行对话 问题在于测试一些用户工作流;像 即使在游戏开始之前,用户也会立即加入和退出 用户在游戏中间离开 用户退出游戏等等 用户工作流与IO()中的所有线程耦合。 我尝试通过传递通道(以值作为前缀)对函数编写单元

我正在哈斯克尔写一个在线游戏。。 我的游戏引擎位于statemonad中,因此测试起来很简单,我们已经为它编写了属性。这就是幸福

但是,服务器有一个用户列表,它位于
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中的工作流逻辑可以分离?”并不十分清楚,而且似乎与“如何测试我的工作流?”不是同一个问题-你应该将每个问题限制为一个问题。