问题
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 mockRandom
; - 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