【问题标题】:@InjectMocks has null dependencies@InjectMocks 具有空依赖项
【发布时间】:2018-05-11 06:15:05
【问题描述】:

我们目前正在使用 JUnit 和 Mockito 来测试我们的代码。

我们有一个类似下面的服务接口。

public interface ServiceA {
     void doSomething();
}

它的实现类如下。

@Service
@Transactional
public class ServiceAImpl implements ServiceA {

     @Inject
     private RepositoryA repA;

     @Inject
     private ShareServiceA sharedServA;

     public void doSomething(){
     }
}

现在,我只想模拟 ServiceAImpl 类的 repA 依赖项。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:test-context.xml" })
@DirtiesContext(classMode= DirtiesContext.ClassMode.AFTER_CLASS)
public class ServiceAImplTest {

   @Mock
   RepositoryA repA;

   @InjectMocks
   ServiceA servA = new ServiceAImpl();

   @Before
   public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        ........
   }
}

调用initMocks后,只启动了ServiceImpl的repA依赖,sharedServA保持为null,当被测类调用sharedServA的方法时,导致null异常。

根据我在互联网和书籍上阅读的一些参考资料,只有在被测试的类具有声明了参数的构造函数时才会发生这种情况。但是,对于我上面的示例,我没有声明任何构造函数。这是正确的行为还是我错过了什么?

【问题讨论】:

  • 您可以通过清理 API 以使用构造函数注入来完全避免这种情况,这样做的好处是根本不需要使用 Spring 进行测试。

标签: java spring junit mocking mockito


【解决方案1】:

继续@daniu 回答

由于混合匹配方法,您被卡住了。

如果您正在为一个单元编写测试,您还必须在测试中满足该单元的依赖关系。

假设你有一个类 A 有两个依赖项 BC 并且你正在使用像这样的设置器将它们注入老式

public class A {

    private B b;

    private C c;

    void method() {
        System.out.println("A's method called");
        b.method();
        c.method();

    }

    public void setB(B b) {
        this.b = b;
    }

    public void setC(C c) {
        this.c = c;
    }
}

那么上面类的单元测试会是这样的

public class ATest {

    A a=new A();

    @Test
    public void test1() {
        B b=new B();
        C c=new C();
        a.setB(b);
        a.setC(c);

        a.method();
    }
}

测试中满足的依赖关系也使用setter注入。这实际上是在 mockito 框架之前测试 java 单元的方式。这里BC 可以是test-doubles 或根据需要实际的类。

但是如果我们使用 spring 在我们的类中使用基于注解的依赖注入,那么我们的 A 类将看起来像

@Service
public class A {

    @Inject
    private B b;

    @Inject
    private C c;

    void method() {
        System.out.println("A's method called");
        b.method();
        c.method();

    }
}

这里我们依靠 @Inject 使用 spring 配置自动注入我们的依赖项。

但是要对这些进行单元测试,我们需要一些框架,例如 mockito,它同样能够这样做,即在没有 setter 或构造函数的情况下注入依赖项。 每个依赖项都可以是mockspy(如果需要实际调用)

因此A 的测试应该是这样的

@RunWith(MockitoJUnitRunner.class)
public class ATest {

    @InjectMocks
    A a;

    @Mock //or @Spy
    B b;

    @Mock //or @Spy
    C c;

    @Test
    public void test() {
        a.method();
    }
}

但你不能混搭。如果您不想使用@InjectMocks 并且不想为每个要实际执行的依赖项使用@Spy,只需@Autowire 或在您的类中实例化A,它将加载A 与所有实际依赖项但随后您将不得不考虑如何覆盖您想要模拟的一两个特定模拟。

1) 您可以为该特定依赖项提供一个设置器,然后您可以创建一个模拟并插入它。

public class ATest {

    @Autowired
    A a;

    @Mock
    B b;

    @Test
    public void test() {
        a.setB(b);
        a.method();
    }
}

2) 即使您的原始类没有为它提供设置器,您也可以成为一个坏蛋并使用 ReflectionTestUtils 动态注入模拟依赖项。

@ContextConfiguration(classes=Config.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class ATest {

    @Autowired
    A a;

    @Mock
    B b;

    @Test
    public void test() {
        ReflectionTestUtils.setField(a, "b", b);
        a.method();
    }
}

3) 同样正如@Yogesh Badke 所提到的,您可以在单独的测试上下文文件中维护此类模拟 bean 并使用该文件,但对于每个此类特定示例,必须再次维护该文件。

【讨论】:

  • 非常感谢您的详细解释。实际上,我也尝试了解决方案 2,但没有奏效。我收到“在目标上找不到 [null] 类型的“b”......”
  • 您的 bean A 被标记为 @Transactional,这可能会导致进一步的问题。请参阅此帖子stackoverflow.com/a/38344022/2179336 这应该会有所帮助。
  • 根据帖子 Spring >4.3.1 应该可以正常工作,否则可以使用此解决方案 stackoverflow.com/a/34477414/2179336
  • 不幸的是,我们使用的是 4.2。但没关系,我能够找到一种解决方法让 RefectionTestUtils.setField() 工作。
【解决方案2】:

你遗漏了一些东西,那就是对服务的嘲弄:

@Mock
private ShareServiceA sharedServA;

在你的测试课上。

@Mock
RepositoryA repA;

是注入模拟存储库的原因。

编辑:

既然你“不想模拟你的 sharedServA”,也许你想

@Spy
private ServiceAImpl sharedServA;

但这通常是不好的测试风格 - Mockito docs state that:

应谨慎使用真正的间谍,偶尔使用,例如在处理遗留代码时。

【讨论】:

  • 感谢您的回答。但是,我不想模拟 sharedServA。我只想模拟 repA 依赖项。
  • @artsylar 然后你的 sharedServA 只能保持为空,因为它是。你想要它是什么?如果您需要服务的实际实例,可以使用@Spy。我编辑了我的答案。
  • 我明白了。因此,如果您在一个类中有很多依赖项,您仍然需要在测试类中使用 @Spy 声明所有依赖项,即使您不会在测试类中模拟其任何方法。
  • @artsylar 好吧,我猜 TL;DR 是“如果你不为变量分配任何东西,它就是 null”(这是你现在看到的)所以你确实需要分配某事。无论是模拟、间谍还是真实的实例化对象。模拟是最好的选择。
【解决方案3】:

当您想模拟一个 bean 但又想拥有另一个 bean 的真实实例时,您需要加载整个 spring 上下文以便正确注入真正的 bean,并且您必须将模拟的 bean 注册到 spring 上下文中以便它被注入了。

因此,在您的一个测试配置类中声明一个 RepositoryA 的模拟 bean。

@Bean
public void mockedRepositoryA() {
   return mock(RepositoryA.class);
}

test-context.xml 中如下

<bean id="idOfServiceAHere" class="org.mockito.Mockito" factory-method="mock">
</bean>

然后通过@Inject注解注入它,就好像它是真正的bean一样。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:test-context.xml" })
@DirtiesContext(classMode= DirtiesContext.ClassMode.AFTER_CLASS)
public class ServiceAImplTest {

   // internally, you will have mocked instance of RepositoryA and
   // real instance of ShareServiceA.
   @Inject
   private ServiceA serviceA;

   @Before
   public void setUp() throws Exception {
        ....
   }
}

【讨论】:

    【解决方案4】:
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "classpath:test-context.xml" })
    @DirtiesContext(classMode= DirtiesContext.ClassMode.AFTER_CLASS)
    public class ServiceAImplTest {
    
       @Mock // not @mock
       RepositoryA repA;
    
       // need to mock each dependency of class under test
       @Mock
       ShareServiceA sharedServA;
    
       // no need to initialize via new
       @InjectMocks
       ServiceA servA;
    
       @Before
       public void setUp() throws Exception {
            // no need for this,
            // @RunWith(SpringJUnit4ClassRunner.class) will be enough
            // MockitoAnnotations.initMocks(this);
            ........
       }
    }
    

    查看代码中的 cmets。

    如果您不想模拟ShareServiceA,您仍然可以模拟它并调用真实方法而不是返回模拟结果。

    Mockito.when(sharedServA.someMethod(any())).thenCallRealMethod();
    

    【讨论】:

    • 感谢您的回答。但是,我不想模拟 sharedServA。我只想模拟 repA 依赖项。
    • 刚刚更新了我的问题。将“@mock”更改为“@Mock”谢谢!
    【解决方案5】:

    您应该为 ShareServiceA sharedServA 创建@Mock。
    因为java中的任何对象都会创建一个默认的构造函数。

    如果没问题。请提供有关 ShareServiceA 的代码。

    【讨论】:

    • 感谢您的回答。但是,我不想模拟 sharedServA。我只想模拟 repA 依赖项。
    • 您可以尝试在 test-context.xml 中添加名称为 sharedServA 的 bean ShareServiceA。测试上下文中的 Spring 自动检查 bean 并自动注入到 ServiceA。
    猜你喜欢
    • 2018-06-01
    • 1970-01-01
    • 2014-08-27
    • 1970-01-01
    • 1970-01-01
    • 2012-11-27
    • 2021-02-15
    • 2019-03-16
    • 1970-01-01
    相关资源
    最近更新 更多