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.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
gotube
  • 127
  • 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 Oct 26 '21 at 05: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 Oct 26 '21 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. – Filip Milovanović Oct 26 '21 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 Oct 27 '21 at 03:24

1 Answers1

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.

candied_orange
  • 102,279
  • 24
  • 197
  • 315
  • 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 Oct 26 '21 at 07: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 Oct 26 '21 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 Oct 26 '21 at 19:13
  • @gotube Kotlin seems to have the same visibility modifiers as Java. So if for some reason the existence of a state setting constructor absolutely must be hidden from the front end you could hide it with package access. Newer Java versions also offer module boundaries. Not sure if Kotlin takes advantage of that. And you could just put a comment on it telling people its intended use is for testing. But seriously, of the things that cause problems by leaking hidden state, constructors do not make the top of my list. Are you sure you don't want anyway to save and load state? – candied_orange Oct 26 '21 at 19:22
  • 1
    Also, the problem you're grappling with seems very similar to something called [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html). You're just coming at if from the other end. Might be worth a look. – candied_orange Oct 26 '21 at 19:36
  • @candied_orange The reason I want to protect the hidden state is that I've heard from many sources that it's best practice. I'm inferring that you agree it's generally important to protect hidden aspects, but it's not always a code smell to expose things for the sake of testability. If so, I'm open to that and would like to learn more, but I don't know what to tell a search engine I'm looking for. Is there an industry term that's more concise than, "relative risk of exposing certain elements of hidden state or implementation details"? – gotube Oct 26 '21 at 20:16
  • @candied_orange And yes, a save/load game feature is a great idea that I'm going to add. However, I think it only sweeps the problem down the hall because implementing that feature leads to the same issue of exposure in that a front end could still generate an invalid state. But if again I'm inferring correctly that in this case it's an acceptable trade-off, I'm fine with that. – gotube Oct 26 '21 at 20:30
  • @gotube you're running head long into an OLD problem. You want to define state in one and only one place. And there are good instincts behind that. But performance gets ignored if you stick with processCommand as the only way to set state. So add other ways. Ways that might break in the future. If people want future proof backups have them write out processCommand files. But fast ways to load the same state don't lock you down if you warned that they might break. Let people save both ways, even in the same file, and now you can convert old save files as your state version changes. – candied_orange Oct 26 '21 at 21:01
  • @candied_orange That's so helpful. Thanks! – gotube Oct 26 '21 at 21:21
  • @candied_orange Is there a reason you didn't suggest using module visibility solution first (in Kotlin, `internal` means module-visible, not package-visible, but they're functionally equivalent in my context of having a single package)? I hadn't realized that my testing suite can see `internal` objects while a third-party front end would not. This seems to me a finger-snap solution that addresses my general question perfectly, but you only mentioned it in a comment as a less desirable solution. – gotube Oct 26 '21 at 21:39
  • 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. – candied_orange Oct 26 '21 at 21:52
  • @candied_orange That's exactly the frame of mind I came here with. I've heard of developers breaking their code to make it more testable and I wanted to avoid that. Thanks again :) – gotube Oct 27 '21 at 03:30