1

I'm struggling to test functionality in a class where the class has to be in a certain state for the functionality to work, but the class cannot be put directly into a given state by design, to maintain state integrity.

I'm developing the back end of a board game in Kotlin. It includes a Game class that is responsible for (1) processing game commands from the player's front-end, and (2) providing information back to the front-end about the game state. I'm trying to write tests for this class, but I can't seem to find a reasonable balance between encapsulation and testing.

The interface visible to the unit tester looks like this:

class Game(numPlayers: Int = SETTINGS_DEFAULT_NUM_PLAYERS) {
    
    fun processCommand(command: Command): String { ... }

    fun getPlayerVictoryPoints(playerIndex: Int): Int { ... }
    fun getAvailableActions(): List<GameAction> { ... }
    fun getPurchasedCards(playerIndex: Int): List<Card> { ... }
    fun gameIsOver(): Boolean { ... }
    .
    .
    .

}

processCommand() is the only method that can affect the state, other than the constructor itself. All other visible class methods return some information about the game state.

The Command object contains a command based on user input, such as "take red currency", "purchase card number 4", "convert blue currency to yellow", and so on. The processCommand() method gets these commands processed.

At the start of the game, players have no currency and no cards purchased. Small randomly selected and rotating assortments of cards and currency are also exposed. Players have to gather the currency and purchase cards from what is exposed. These cards give players special abilities, like converting currency from one type to another, or granting victory points upon performing specific game actions such as purchasing a card. The game ends when any player has purchased more than a certain number of cards.

To test processCommand() with a Command that converts currency types, for instance, the player has to have purchased a card that allows such a conversion. The case is similar with several other Commands. For a method like getPlayerVictoryPoints(), a player needs to have already purchased a card that grants victory points, and then be able to perform the specific action that triggers the victory points. Testing whether the gameIsOver() method ever returns true requires a complete game to be played out.

As processCommand() is the only method that takes any input, and it's impossible to predict what currency and cards are available, I believe the core functionality of my game is currently impossible to test.

Here's some options that are on the table, in no particular order:

  1. add internal (module-visibile) methods to Game class that expose implementation details
  2. increase visibility on existing private and protected methods and properties to internal
  3. refactor Game to an open class, with visibility on all its private methods and properties widened to protected (visible to subclasses), and create a TestingGame subclass of Game with extra methods that make setting state simple

I believe all of those violate encapsulation principles because a third-party front end would have access to the implementation details and be able to set invalid states.

I've had this problem before with similar game projects, so I'm looking for explanations of broader methods and principles that address this problem, and perhaps how they apply to this code as a bonus.

I've read up on mocking, but since this isn't an issue with an outside dependency, I don't think it applies here. Also, I understand what TDD is and how it may have helped me if I'd started out with that, but I'm fairly certain I would have arrived at this same question, just during the design phase rather than while coding.

4
  • 3
    The implication of this question is that there’s no way to save and reload the current game. That seems like a big missing feature and of course with that feature your issue of how to set up the game state goes away,.,
    – David Arno
    Commented Oct 26, 2021 at 5:31
  • @DavidArno Yes, I'm going to implement a save game feature. I don't see how this will solve the issue rather than moving it elsewhere. There's a comment thread about this on candied_orange's answer below.
    – gotube
    Commented Oct 26, 2021 at 20:39
  • 1
    Think of a test as of an "exercising example" of client code that is going to call the methods in your Game class. Then think about what that client code is going to call, and what it needs to know about Game, what assumptions it can make. Test that, and nothing else, at least not within that set of tests. Then go inside your Game class and see if it's doing too much (it probably is). Extract that into separate classes; then see what you need to write in the Game class in order to use them. Write tests for the extracted classes that check assumptions for the code you'd write in Game. Commented Oct 26, 2021 at 22:10
  • @FilipMilovanović I'm not sure what to infer from this very general advice about my problem. My Game class takes a command, attempts to update the game state with it, then returns a result code. The client can expect that if it sends a valid command like "convert red currency to yellow" when the game state is correct that it will work. I can only test this by artificially manipulating the game state. Can you please clarify how this applies to my problem?
    – gotube
    Commented Oct 27, 2021 at 3:24

1 Answer 1

5

Tests should run fast.

Now sure, you could set up each test by using processCommand to process every commend needed to get the game into the state you want to test but that's pitifully slow.

The alternative is to load in game state from some kind of cache or serialization. Heck even calling a constructor could do this. Yeah, that exposes state.

But come on. It's only exposed to the tests that use it. And it's hidden as soon as it's constructed. So this knowledge isn't going to spread to the rest of the project. It's not like you have getters right?

Yes, you could use some mocking voodoo or reflection magic to set internal state but a simple constructor can keep your secrets without being all weird. And it's fast, so your tests will be fast. Which means you'll actually run them often enough for them to do some good.

12
  • Do you mean there's a way to load a state in the testing suite that would be unavailable to the front-end? I wouldn't want the front-end able to do that. No, I have no getters. Where would this constructor reside that would keep it secret from the front-end?
    – gotube
    Commented Oct 26, 2021 at 7:11
  • 1
    You don't hide it per-se, but you have it take a type that you don't construct in the front end. Does your game have a "Save -> Load" feature?
    – Caleth
    Commented Oct 26, 2021 at 13:45
  • @Caleth Am I wrong that any classes and methods visible to the testing suite would also be visible to the front end? If so, then the front end would be as capable of generating states as the testing suite. Keep in mind, I'm only producing a back end that someone else would be able to develop a front end for, so other than visibility, I have no control over what the front end constructs or doesn't.
    – gotube
    Commented Oct 26, 2021 at 19:13
  • 1
    Also, the problem you're grappling with seems very similar to something called Event Sourcing. You're just coming at if from the other end. Might be worth a look. Commented Oct 26, 2021 at 19:36
  • 1
    I’m hesitant because creating production code for the sake of testing is the start of a slippery slope. I’m also not used to this situation. I’m used to the one where we need a way to set state operationally. Only doing it for testing is strange to me. Commented Oct 26, 2021 at 21:52

Not the answer you're looking for? Browse other questions tagged or ask your own question.