【问题标题】:Breaking a local dependency to unit test a void method打破本地依赖以对 void 方法进行单元测试
【发布时间】:2013-02-27 07:37:07
【问题描述】:

我正在使用 mockito 进行练习,但对于如何测试依赖于本地对象中的方法调用的方法,我有点卡住了。 请参阅以下示例:

public class Worker {          

    public void work() {
                   Vodka vodka = new Vodka();
                   vodka.drink();
     }
}

这个工人,不做他的工作,他喜欢喝酒。但是我想添加一个测试来证明他在工作时喝酒。但是没有办法这样做,因为我必须在调用方法 work 时验证方法 drink() 是否被调用。我想你同意我的观点,这是不可能测试的,所以我需要在开始测试之前打破依赖关系。 这是我的第一个疑问,您认为打破这种依赖的最佳方法是什么? 如果我只是将伏特加对象的范围更改为全局,我认为不会很好(我不想将它暴露给班级的其他部分)。我想过创建一个工厂,像这样:

public class Worker {          

    private VodkaFactory vodkaFactory = new VodkaFactory();


    public void work() {
                   Vodka vodka = vodkaFactory.getVodka();
                   vodka.drink();
     }
}

我不确定我是否正确地破坏了依赖关系,但我现在想做的是测试在执行 work() 时是否调用了方法 drink()。 我试过这个没有运气:

@Test
    public void
    does_the_worker_drink_while_working
    () {
        VodkaFactory vodkaFactory = mock(VodkaFactory.class);
        Vodka vodka = mock(Vodka.class);
        Worker worker = new Worker();
        worker.work();
        when(vodkaFactory.getVodka()).thenReturn(vodka);
        verify(vodka,times(1)).drink();
    }

我模拟工厂,何时会检测到工厂创建了一个新的伏特加对象。但是当我验证该方法调用了 1 次方法drink() 时,mockito 告诉我:

Wanted but not invoked:
vodka.drink();
-> at testing_void_methods_from_local_objects.WorkerSpecification.does_the_worker_drink_while_working(WorkerSpecification.java:22)
Actually, there were zero interactions with this mock.

我没有正确存根或者我做错了什么。你能帮我完成这个测试,并澄清一下测试这些无法测试的方法的最佳方法是什么?

我知道 mockito 有一个名为 doAnswer() 的方法,它用于模拟方法调用,你认为它在这种情况下有用吗? 我应该如何使用它?

更新:

我正在按照建议在 work() 之前调用 when(),并且我正在尝试允许从类外部设置工厂:

@Test
public void
does_the_worker_drink_while_working
() {
    VodkaFactory vodkaFactory = mock(VodkaFactory.class);
    Vodka vodka = mock(Vodka.class);
    Worker worker = new Worker();
    when(vodkaFactory.getVodka()).thenReturn(vodka);
    worker.work();
    verify(vodka,times(1)).drink();
}

现在这是生产代码:

public class Worker {          


        private VodkaFactory vodkaFactory;

        public void work() {
                       Vodka vodka = vodkaFactory.getVodka();
                       vodka.drink();
         }

         public void setVodkaFactory(VodkaFactory vodkaFactory) {
               this.vodkaFactory = vodkaFactory;
         }

我得到的异常如下:

 java.lang.NullPointerException
        at testing_void_methods_called_from_local_objects.Worker.work(Worker.java:9)

这行写着vodka.drink()

抱歉,我仍然对问题所在感到困惑。

【问题讨论】:

  • 正如你们中的一些人提到的,我做的第一件事是在work() 方法之前调用when。所以我在when(vodkaFactory.getVodka()).thenReturn(vodka); 这一行向上移动了 1 行,我运行了测试,但它在vodka.drink(); 处出现空指针而失败,我有点不知道下一步该做什么。我不想将工厂包含在工人的构造函数中,我为什么要这样做?
  • 因为我记得看到你的另一个单元测试问题,我建议你买一本应该回答大多数常见问题的初学者书。对于这个特定的问题,请查看这个问题 - stackoverflow.com/questions/246038。您不必更改方法签名来测试它。相反,看看如果 Worker 喝了伏特加会发生什么可观察到的变化。我确定有一个。
  • 谁说不能测试?您可以使用几种不同的模拟工具(PowerMock、JMockit)来为work 方法编写单元测试,而完全不改变它。您只需要模拟内部创建的 Vodka 对象,实际上,这两种工具都很容易做到。
  • @Rogerio 您能否提供一些示例来说明如何在不破坏依赖关系的情况下进行测试? PowerMock、JMock 和一些工具可能让你测试私有方法和硬依赖,但我认为这些方法并不总是可取的。我不同意你的看法。
  • @sfrj 我发布了一个带有示例测试的答案。为什么这样的事情是不可取的?我更喜欢它在被测代码中创建不必要的复杂性,例如 VodkaFactory 类。

标签: java unit-testing tdd mockito legacy


【解决方案1】:

您的工人在这里创建自己的工厂类:

private VodkaFactory vodkaFactory = new VodkaFactory();

您正在创建的模拟与工作实例完全分离,因此缺乏交互。为了让它发挥作用,工厂必须从“外部”注入工人,比如通过constructor injection

如果这是遗留代码,您可以使用反射将私有工厂实例替换为模拟的。

正如 JB Nizet 在评论中所指出的,您的模拟设置是在 work 已被调用之后进行的。为了把事情做好,在调用任何使用它的代码之前注入 mock 并设置它。

【讨论】:

  • +1。另一个问题是work() 方法是在工厂被存根之前调用的,而不是之后。
  • @JBNizet:谢谢,我已将您的评论纳入我的回答中。
  • @jimmy_keen 我想我按照你们的建议尝试了,但仍然不适合我(请参阅更新)。我很困惑,注入模拟是什么意思?
  • @sfrj:你忘记将模拟工厂注入 Worker:worker.setVodkaFactory(vodkaFactory)。我会使用构造函数参数而不是 setter 来执行此操作。
  • @jimmy_keen 是的,我刚刚看到了 :) 刚刚修好了。我知道我很接近。感谢您的帮助。
【解决方案2】:

您尝试测试的代码中存在逻辑错误。因为您在 Worker 类中创建了 VodkaFactory 实例,而且您已将该字段设为私有。

最好的解决方案是从班级外部传递对VodkaFactory 的引用。

public class Worker {          

    private VodkaFactory vodkaFactory;

    public void work() {
                   Vodka vodka = vodkaFactory.getVodka();
                   vodka.drink();
     }

     public void setVodkaFactory(VodkaFactory vf) {
           vodkaFactory = vf;
     }

}

现在,在您的 @Test 中,您可以使用 setVodkaFactory setter 传递您模拟的 VodkaFactory 实例。

【讨论】:

  • 感谢您的回答+1
【解决方案3】:

更多的是评论而不是答案。除了使工厂成为可注入的依赖项之外,您还可以确保在与模拟 when(vodkaFactory.getVodka()).thenReturn(vodka); 交互之前对其进行训练 worker.work();

【讨论】:

  • 感谢您的回答+1
【解决方案4】:

你需要设置你的伏特加工厂:

@Test
public void
does_the_worker_drink_while_working() {
    VodkaFactory vodkaFactory = mock(VodkaFactory.class);
    Vodka vodka = mock(Vodka.class);
    Worker worker = new Worker();
    when(vodkaFactory.getVodka()).thenReturn(vodka);

    //call your setter        
    worker.setVodkaFactory(vodkaFactory);

    worker.work();
    verify(vodka,times(1)).drink();
}

【讨论】:

  • 谢谢,是的,这就是解决方案。但是我会奖励最佳答案给jimmi,因为他首先回答了正确的答案。 +1
【解决方案5】:

以下是一个完整的JMockit 单元测试,它在独立于其Vodka 依赖项的实现的情况下执行Worker#work() 方法:

@Test
public void workTest(@Mocked final Vodka mockBeverage)
{
    new Worker().work();

    new Verifications() {{ mockBeverage.drink(); times = 1; }};
}

【讨论】:

  • 回复您上面的评论。 I much prefer it to creating unnecessary complexity in the code under test我仍然不同意:我提到的初始代码是不可测试的,因为具有本地范围Vodka vodka = new Vodka();正在调用外部逻辑,而被测方法取决于构造函数Vodka()在这种情况下很简单,但调用可以更复杂,所以我认为这是在添加测试覆盖之前应该打破的依赖关系。如果不是这样,那么它将是一个集成测试,而不是一个单元测试。无论如何,感谢您的回答+1
  • 我证明了它可测试的(见上面的测试),而且绝对是一个真正的单元测试。它也不再困难,也适用于更复杂的情况。调用或“依赖”构造函数不是“外部逻辑”或任何问题,只是常规应用程序代码 - 除非您真的想通过要求使用诸如工厂、服务定位器或即使在实际需求没有要求的情况下也可以进行依赖注入。
  • 好的,我试试看。这是什么框架?我经常使用 Mockito,我发现我没有那个 @Mocked 注释。
  • 它是 JMockit。我在答案中添加了指向项目站点的链接。您还可以使用 PowerMock,它为 Mockito 提供了扩展 API:whenNew(Vodka.class).withNoArguments().thenReturn(mockBeverage)
  • 谢谢,我去看看。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-01
  • 2011-07-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-07-21
相关资源
最近更新 更多