Exit e-book
Show all chapters
15
Writing GameModeSpec
15. 
Writing GameModeSpec

Sign up to our Newsletter

Signing up to our newsletter allows you to read all our ebooks.

I agree to receive marketing communication from Scalac.
You can unsubscribe from these communications at any time. For more information on how to unsubscribe, view our Privacy Policy.

Mastering Modularity in ZIO with Zlayer
15

Writing GameModeSpec

In this case, let’s concentrate on just one test instead of the whole suite:


testM("returns state with added piece and turn advanced to next player if field is unoccupied") {
 val gameCommandParserMock: ULayer[GameCommandParser] =
   GameCommandParserMock.Parse(equalTo("put 6"), value(GameCommand.Put(Field.East)))
 val gameLogicMock: ULayer[GameLogic] =
   GameLogicMock.PutPiece(
     equalTo((gameState.board, Field.East, Piece.Cross)),
     value(pieceAddedEastState.board)
   ) ++
     GameLogicMock.GameResult(equalTo(pieceAddedEastState.board), value(GameResult.Ongoing)) ++
     GameLogicMock.NextTurn(equalTo(Piece.Cross), value(Piece.Nought))
 val env: ULayer[GameMode] =
   (gameCommandParserMock ++ GameView.dummy ++ OpponentAi.dummy ++ gameLogicMock) >>> GameMode.live

 val result = GameMode.process("put 6", gameState).provideLayer(env)
 assertM(result)(equalTo(pieceAddedEastState))
}

You can see the above test is for GameMode.process, and GameMode depends on several modules: GameCommandParser, GameView, OpponentAi and GameLogic. So, for being able to run the test, we need to provide mocks for those modules, and that’s what’s precisely happening in the above lines. First, we write a mock for GameCommandParser:

val gameCommandParserMock: ULayer[GameCommandParser] =
    GameCommandParserMock.Parse(equalTo("put 6"), value(GameCommand.Put(Field.East)))

As you may have realized, this line depends on a GameCommandParserMock object, and we are stating that when we call GameCommandParser.parse with an input equal to “put 6”, it should return a value of GameCommand.Put(Field.East). By the way, the GameCommandParserMock is defined in the mocks.scala file:

@mockable[GameCommandParser.Service]
object GameCommandParserMock

As you can see, we are now using the @mockable annotation that is included in the zio-test library. This annotation is a really nice macro that generates a lot of boilerplate code for us automatically, otherwise we would need to write it by ourselves. For reference, here is the generated code:

object GameCommandParserMock extends Mock[GameCommandParser] {
 object Parse extends Effect[String, AppError, GameCommand]

 val compose: URLayer[Has[Proxy], GameCommandParser] =
   ZLayer.fromServiceM { proxy =>
     withRuntime.map { rts =>
       new GameCommandParser.Service {
         def parse(input: String): IO[AppError, GameCommand] = proxy(Parse, input)
       }
     }
   }
}

I won’t go into more details about how mocks work in zio-test, however if you want to know more about this you can take a look at the ZIO documentation page.

Then we have to write a mock for GameLogic:

val gameLogicMock: ULayer[GameLogic] =
 GameLogicMock.PutPiece(
   equalTo((gameState.board, Field.East, Piece.Cross)),
   value(pieceAddedEastState.board)
 ) ++
   GameLogicMock.GameResult(equalTo(pieceAddedEastState.board), value(GameResult.Ongoing)) ++
   GameLogicMock.NextTurn(equalTo(Piece.Cross), value(Piece.Nought))

As you can see, the idea here is pretty much the same as how we defined gameCommandParserMock:

  • The mock is defined as a ZLayer.
  • We need to define a GameLogicMock object, similarly as we did above for GameCommandParserMock.
  • For combining expectations sequentially, we use the ++ operator (which is just an alias for the Expectation#andThen method).

Next, we should define mocks for GameView and OpponentAi. However, there’s a problem with that, the reason is that these modules are not actually called by GameMode.process (which is the function being tested), so these mocks should say that we expect them not to be called, but in the current ZIO version there’s no way of stating that (hopefully this will be added in a future release). So, instead of defining mocks, we can define dummy implementations for GameView and OpponentAi:


object GameView {
    ...
    object Service {
      ...
      val dummy: ULayer[GameView] = ZLayer.succeed {
        new Service {
          override def header(result: GameResult, turn: Piece, player: Player): UIO[String] =       
            UIO.succeed("")
          override def content(board: Map[Field, Piece], result: GameResult): UIO[String]   = 
            UIO.succeed("")
          override def footer(message: GameFooterMessage): UIO[String]                      = 
            UIO.succeed("")
        }
      }



object OpponentAi {
    ...
    object Service {
      ...
      val dummy: ULayer[OpponentAi] = ZLayer.succeed {
        new Service {
          override def randomMove(board: Map[Field, Piece]): IO[AppError, Field] = 
            IO.fail(FullBoardError)
        }
      }
    }
    ...
}

Next, once we have defined our mocks and dummy implementations, we need to build the environment for running the test:

val env: ULayer[GameMode] =
  (gameCommandParserMock ++ GameView.dummy ++ OpponentAi.dummy ++ gameLogicMock) >>> GameMode.live

As you can see, building the environment is just a matter of applying horizontal and vertical composition of ZLayers.

Finally, the environment can be provided to the test using ZIO#provideLayer.

PREVIOUS
Chapter
14
NEXT
Chapter
16