1

I'm studying to be a Java developer and right now I'm learning test driven development, which means that im very new to JUnit and Mockito.

I've been struggling for a while now and I'm stuck.

I have no idea how to test this particular method that has no arguments, no return value and a randomizer.

Old logic:

public void getPlayerToStart(int randomNr) {
    if (randomNr == 1) {
        currentPlayer = p1;
        opponentPlayer = p2;
    } else {
        currentPlayer = p2;
        opponentPlayer = p1;
    }
}

Old test

@Test
void testSetCurrentPlayerSetToPlayer1() {
    gameEngine.getPlayerToStart(1);
    assertEquals(gameEngine.getP1(), gameEngine.getCurrentPlayer());
    assertEquals(gameEngine.getP2(), gameEngine.getOpponentPlayer());
}

@Test
void testSetCurrentPlayerSetToPlayer2() {
    gameEngine.getPlayerToStart(2);
    assertEquals(gameEngine.getP2(), gameEngine.getCurrentPlayer());
    assertEquals(gameEngine.getP1(), gameEngine.getOpponentPlayer());
}

New logic:

public void getPlayerToStart() {
    Random rand = new Random();
    int randomNr = rand.nextInt(2) + 1;
    if (randomNr == 1) {
        currentPlayer = p1;
        opponentPlayer = p2;
    } else {
        currentPlayer = p2;
        opponentPlayer = p1;
    }
}

I'm not sure how to be able to test the getPlayerToStart() without the argument "randomNr".. Can someone please just point me in the right direction!

Thanks in advance.

KENUBBE
  • 79
  • 1
  • 5
  • 12

4 Answers4

4

Always to please be keeping in mind that the thought "gee, this is hard to test" is TDD trying to scream at you that the design needs review.

I have no idea how to test this particular method that has no arguments, no return value and a randomizer.

Random numbers are a side effect, like I/O or time, and should be handled that way in your design.

Which is to say, if you are doing TDD, one of the things you should be recognizing is that the source of randomness is an input to your system; it's part of the imperative shell which is provided by your test harness when running tests, and is provided by your composition root in production.

The testable approach would separate "generate a seed" from "compute a state from the seed"; unit tests are great for the latter bit, because pure functions are really easy to test. Generating random numbers is state of sin level hard to test, but with some design you can simplify the code around it to the point that it "obviously has no deficiencies".

You may also want to review Writing Testable Code, by Misko Hevery, or Tales of the Fischer King.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
3

another solution could be a strict interpretation of the single responsibility pattern: a class providing business logic sould not be responsible to create or acquire its dependencies. This leads to the concept of dependency injection:

class CodeUnderTest {
    private final Random rand;
    public CodeUnderTest(@NotNull Random rand){
        this.rand = rand;
    }

    public void getPlayerToStart() {
        int randomNr = rand.nextInt(2) + 1;
        if (randomNr == 1) {
            currentPlayer = p1;
            opponentPlayer = p2;
        } else {
            currentPlayer = p2;
            opponentPlayer = p1;
        }
    }
}

You'd need to enhance your Test to this:

class CodeUnderTestTest{
   private final Random fakeRandom = new Random(1);
   private CodeUnderTest cut;
   @Before
   public void setup(){
       cut = new CodeUnderTest(fakeRandom);
   }

   // your test code relying on the repeatable order
   // of Random object initialized with a fix seed.
}

You also need to change all places in your code where you instantiate CodeUnderTest to add a Random object without seed. This looks like a downside at first but it provides the possibility to have only one instance of Random throughout your code without implementing the Java Singelton Pattern.

You could gain even more control if you replace the Random object with a mock. The easiest way to do that is to use a mocking framework like Mockito:

class CodeUnderTestTest{       
   @Rule
   public MockitoRule rule = MockitoJUnit.rule();
   @Mock
   private Random fakeRandom;

// you could use @InjectMocks here
// instead of the setup method 
   private CodeUnderTest cut;
// This will NOT raise compile errors
// for not declared or not provided 
// constructor arguments (which is bad in my view).

   @Before
   public void setup(){
       cut = new CodeUnderTest(fakeRandom);
   }

   @Test
    void testSetCurrentPlayerSetToPlayer1() {
        doReturn(0).when(fakeRandom).nextInt(2);
        cut.getPlayerToStart(1);
        assertEquals(cut.getP1(), cut.getCurrentPlayer());
        assertEquals(cut.getP2(), cut.getOpponentPlayer());
    }
}
Timothy Truckle
  • 15,071
  • 2
  • 27
  • 51
1

Move the call to new Random() into its own method, like this.

You can rewrite your getPlayerToStart method to use the other one, to save duplicated code (but you don't need to).

public void getPlayerToStart() {
    Random rand = makeRandom();
    int randomNumber = rand.nextInt(2) + 1
    getPlayerToStart(randomNumber);
}

public Random makeRandom() {
    return new Random();
}

Now you can use Mockito to

  • make a mock Random object;
  • make a spy of your class, which is the object you're going to test;
  • stub the makeRandom method of your spy, so that it returns your mock Random;
  • stub the mock Random so that it returns whichever value you like, in each test.

After that, you can write a test in which player 1 is expected to start, and another test in which player 2 is expected to start.

Dawood ibn Kareem
  • 77,785
  • 15
  • 98
  • 110
  • @Test void testSetCurrentPlayerSetToPlayer1() { when(gameEngineSpy.makeRandom()).thenReturn(randMock); when(randMock).thenReturn(1) <--- error here; } thenReturn is expecting a "Random value" I guess that's not what you meant :P, what did i miss? – KENUBBE Nov 02 '18 at 00:52
  • `when(randMock.nextInt()).thenReturn(1);` – Dawood ibn Kareem Nov 02 '18 at 01:13
  • Yes I figured that out when I wrote it here, now it works and I did some experiment with the code so now I know exactly what it does!! Thanks man – KENUBBE Nov 02 '18 at 01:17
  • 1
    Instead introducing an additional method in the classes *interface* the better suggestion would be to use *dependency injection*, preferably via *constructor* to bring the `Random` object into the code under test. At least your suggestion should support *interface segregation* by declaring the additional Method `package private`. – Timothy Truckle Nov 03 '18 at 09:09
  • @Timothy You're welcome to write your own answer if you know a better way of doing this. – Dawood ibn Kareem Nov 03 '18 at 09:55
  • @DawoodibnKareem The basic idea stays the same as yours. My own answer would not be needed if you dare to enhance yours. – Timothy Truckle Nov 03 '18 at 10:05
  • It's generally a bad idea to mock what you don't own. You don't own the contract that this collaborator is exposing, so in case the contract changes, you won't have any effect of that in unit tests, but you will have an unexpected behaviour in production. See here: https://github.com/testdouble/contributing-tests/wiki/Don't-mock-what-you-don't-own – Giancarlo Di Paolantonio Nov 12 '18 at 19:24
1

I agree with who says that you should use dependency injection and have your own abstraction (in this way you could mock the collaborator). But, creating the abstraction you're simply moving the responsibility (and the test problem) elsewhere.

Did you know about the Random constructor that takes an integer argument called "seed"? Using the same seed you will have always the same sequence of results.

See: https://stackoverflow.com/a/12458415/5594926