为什么我的类在单元测试中不调用我的模拟方法?

kadbb459  于 2022-10-15  发布在  Java
关注(0)|答案(1)|浏览(382)

我已经实现了一个应该进行单元测试的类。注意,这里显示的代码只是一个伪实现,Random只是为了说明问题。真正的代码将使用真正的依赖项,例如另一个服务或存储库。

public class MyClass {
  public String doWork() {
    final Random random = new Random(); // the `Random` class will be mocked in the test
    return Integer.toString(random.nextInt());
  }
}

我想用Mockito模拟其他类,并编写了一个非常简单的JUnit测试。然而,我的班级在测试中没有使用模拟:

public class MyTest {
  @Test
  public void test() {
    Mockito.mock(Random.class);
    final MyClass obj = new MyClass();
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
    // this fails, because the `Random` mock is not used :(
  }
}

即使使用MockitoJUnitRunner(JUnit 4)运行测试,或使用m1n 2o1p进行扩展(JUnit5)并用@Mock注解也无济于事;实际实现仍在使用:

@ExtendWith(MockitoExtension.class)   // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class MyTest {
  @Mock
  private Random random;

  @Test
  public void test() {
    final MyClass obj = new MyClass();
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
    // `Random` mock is still not used :((
  }
}

为什么没有使用mocked类,即使在测试我的类或使用Mockito扩展/运行程序执行测试之前调用了Mockito方法?

9jyewag0

9jyewag01#

问题概述(类与示例)

模型是示例(这就是为什么它们也称为“模拟对象”)。对类调用Mockito.mock将返回该类的模拟对象。它必须被赋值给一个变量,然后该变量可以传递给相关方法或作为依赖注入到其他类中。它不会不会修改类本身!想想看:如果这是真的,那么类的所有示例都会神奇地转换为mock。这将使得无法模拟使用多个示例的类或来自JDK的类,例如ListMap(首先不应该模拟这些类,但这是另一回事)。
使用Mockito扩展名/runner的@Mock注解也是如此:创建一个模拟对象的新示例,然后将其分配给用@Mock注解的字段(或参数)。这个模拟对象仍然需要传递给正确的方法或作为依赖注入。
另一种避免混淆的方法是:Java中的new将“始终”为对象分配内存,并将初始化真实类的这个新示例。无法重写new的行为。即使像Mockito这样的聪明框架也无法做到这一点。

解决方案

»但我怎么能嘲笑我的班级呢?«你会问的。将类的设计更改为可测试的!每次您决定使用new时,都会将自己提交给这种类型的示例。根据您的具体用例和要求,存在多种选项,包括但不限于:
1.如果可以更改方法的签名/接口,请将(mock)示例作为方法参数传递。这要求示例在所有调用站点中都可用,这可能并不总是可行的。
1.如果无法更改方法的签名,请在构造函数中输入inject the dependency,并将其存储在一个字段中,以便以后方法使用。
1.有时,示例只能在调用方法时创建,而不能在调用之前创建。在这种情况下,可以引入另一个间接级别,并使用abstract factory pattern。然后,工厂对象将创建并返回依赖项的示例。工厂可以存在多个实现:一个返回实际依赖项,另一个返回双重测试,例如模拟。
以下是每个选项的示例实现(有无Mockito runner/extension):

更改方法签名

public class MyClass {
  public String doWork(final Random random) {
    return Integer.toString(random.nextInt());
  }
}

public class MyTest {
  @Test
  public void test() {
    final Random mockedRandom = Mockito.mock(Random.class);
    final MyClass obj = new MyClass();
    Assertions.assertEquals("0", obj.doWork(mockedRandom)); // JUnit 5
    // Assert.assertEquals("0", obj.doWork(mockedRandom));  // JUnit 4
  }
}

@ExtendWith(MockitoExtension.class)   // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class MyTestAnnotated {
  @Mock
  private Random random;

  @Test
  public void test() {
    final MyClass obj = new MyClass();
    Assertions.assertEquals("0", obj.doWork(random)); // JUnit 5
    // Assert.assertEquals("0", obj.doWork(random));  // JUnit 4
  }
}

构造函数依赖注入

public class MyClass {
  private final Random random;

  public MyClass(final Random random) {
    this.random = random;
  }

  public String doWork() {
    return Integer.toString(random.nextInt());
  }
}

public class MyTest {
  @Test
  public void test() {
    final Random mockedRandom = Mockito.mock(Random.class);
    final MyClass obj = new MyClass(mockedRandom);
    // or just obj = new MyClass(Mockito.mock(Random.class));
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
  }
}

@ExtendWith(MockitoExtension.class)   // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class MyTestAnnotated {
  @Mock
  private Random random;

  @Test
  public void test() {
    final MyClass obj = new MyClass(random);
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
  }
}

工厂延迟施工

根据依赖项的构造函数参数的数量和表达代码的需要,可以使用JDK中的现有接口(SupplierFunctionBiFunction)或引入自定义工厂接口(如果只有一个方法,则用@FunctionInterface注解)。
下面的代码将选择自定义接口,但与Supplier<Random>配合使用效果很好。

@FunctionalInterface
public interface RandomFactory {
  Random newRandom();
}

public class MyClass {
  private final RandomFactory randomFactory;

  public MyClass(final RandomFactory randomFactory) {
    this.randomFactory = randomFactory;
  }

  public String doWork() {
    return Integer.toString(randomFactory.newRandom().nextInt());
  }
}

public class MyTest {
  @Test
  public void test() {
    final RandomFactory randomFactory = () -> Mockito.mock(Random.class);
    final MyClass obj = new MyClass(randomFactory);
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
  }
}

@ExtendWith(MockitoExtension.class)   // JUnit 5
// @RunWith(MockitoJUnitRunner.class) // JUnit 4
public class MyTestAnnotated {
  @Mock
  private RandomFactory randomFactory;

  @Test
  public void test() {
    // this is really awkward; it is usually simpler to use a lambda and create the mock manually
    Mockito.when(randomFactory.newRandom()).thenAnswer(a -> Mockito.mock(Random.class));
    final MyClass obj = new MyClass(randomFactory);
    Assertions.assertEquals("0", obj.doWork()); // JUnit 5
    // Assert.assertEquals("0", obj.doWork());  // JUnit 4
  }
}

相关问题