如何使用 JUnit 或 Mockito 测试使用 Random()、不带参数和 return 值的方法

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

我正在学习成为一名 Java 开发人员,现在我正在学习测试驱动开发,这意味着我对 JUnit 和 Mockito 还很陌生。

我已经挣扎了一段时间了,我卡住了。

我不知道如何测试这个没有参数、没有 return 值和随机发生器的特定方法。

旧逻辑:

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

旧测试

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

新逻辑:

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;
    }
}

我不确定如何在没有参数的情况下测试 getPlayerToStart() "randomNr"。有人能给我指出正确的方向吗!

提前致谢。

将对 new Random() 的调用移动到它自己的方法中,就像这样。

您可以重写 getPlayerToStart 方法以使用另一个方法,以保存重复的代码(但您不需要)。

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

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

现在您可以使用 Mockito

  • 制作一个模拟 Random 对象;
  • 监视你的 class,这是你要测试的对象;
  • 存根你的间谍的 makeRandom 方法,这样它 returns 你的模拟 Random;
  • 存根模拟 Random 以便它 returns 在每个测试中你喜欢的任何值。

之后,您可以编写一个测试,让玩家 1 开始,另一个测试让玩家 2 开始。

请始终牢记,"gee, this is hard to test" 的想法是 TDD 试图向您尖叫 设计需要审查。

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

随机数是一种副作用,如 I/O 或时间,应在您的设计中以这种方式处理。

也就是说,如果您正在进行 TDD,您应该认识到的一件事是随机性来源是您系统的 输入 ;它是 imperative shell 的一部分,它在 运行 测试时由您的测试工具提供,在生产中由您的组合根提供。

可测试的方法会将 "generate a seed" 与 "compute a state from the seed" 分开;单元测试非常适合后者,因为纯函数真的很容易测试。生成随机数是 state of sin 级难以测试的,但通过一些设计,您可以将围绕它的代码简化到 "obviously has no deficiencies".

的程度

您可能还想查看 Writing Testable Code, by Misko Hevery, or Tales of the Fischer King

另一种解决方案可能是对单一责任模式的严格解释:class提供业务逻辑不应该是负责创建或获取其依赖项。这就引出了依赖注入的概念:

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;
        }
    }
}

您需要为此增强您的测试:

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.
}

您还需要更改代码中实例化 CodeUnderTest 的所有位置,以添加不带种子的 Random 对象。 起初这看起来像是一个缺点,但它提供了在整个代码中只有一个 Random 实例而无需实现 Java Singelton Pattern.[=17 的可能性=]

如果将 Random 对象替换为 mock,您可以获得更多的控制权。最简单的方法是使用 mocking 框架,例如 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());
    }
}

我同意谁说你应该使用依赖注入并拥有你自己的抽象(这样你就可以模拟合作者)。 但是,创建抽象只是将责任(和测试问题)转移到别处。

您是否知道接受名为 "seed" 的整数参数的 Random 构造函数?使用相同的种子,您将始终获得相同的结果序列。

参见: