How to test a method that uses Random(), without arguments and return value, using JUnit or Mockito

大城市里の小女人 提交于 2020-07-09 13:19:06

问题


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.


回答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.




回答2:


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.




回答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());
    }
}



回答4:


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



来源:https://stackoverflow.com/questions/53110890/how-to-test-a-method-that-uses-random-without-arguments-and-return-value-usi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!