我应该在测试方法中模拟本地方法调用吗?

时间:2014-05-14 11:35:46

标签: java unit-testing testing mocking stubbing

关于单元测试概念的问题:

class A {
   public void m1() {
      // code
      m2();
      //code
   }

   public void m2() {
      //some code
   }

}

根据最佳做法,我应该如何测试m1方法?单位是类还是方法的单位?

我的观点 - 我应该单独测试m2,我不应该测试m1m2集成。

使用我的观点很难 - 我应该使用复杂的框架来测试和使用非常现代的东西。

根据我的单元测试感,测试应该很简单!如果您的代码是好的,您可以在没有复杂功能的情况下进行测试但是在m1()中调用m2()是正常的代码。

请澄清我的误解。

更新

模拟示例(伪代码):

//prepare
A testClass = mock(A.class);
when(testClass.m2()).doNothing();
when(testClass.m1()).callRealMethod();
//execute:
testClass.m1();
//verify
check testClass state after method m1 invocation.

这就是我测试一个模拟类的方法。这是正常的吗?

4 个答案:

答案 0 :(得分:3)

首先,在进行单元测试时,测试所有公共方法。在您的示例中,m1m2是公开的,因此您可以对两者进行测试。

您可能想要存根或模拟m2有几个原因:

  1. 如果在测试m1时遇到任何问题,因为m1调用m2,存根或模拟m2。您可能遇到的一些问题:

    • m2可能会调用外部服务
    • m2可能只是很慢
    • 可能很难使用满足m1的参数调用m2(您的m2没有参数,但我通常会说话)
  2. 有时候,当你测试一个调用另一个方法的方法并测试另一个方法时,你会发现两个方法的测试之间存在重复 - 调用方法的一些测试确实在测试被叫方法。通过在调用方法中存根或模拟被调用的方法来处理它,测试调用方法就足以证明调用被调用的方法,并彻底测试被调用的方法。

  3. 如果您执行TDD,则可以在编写m1之前编写m2。然后,您将存根或模拟m2,以便m1的测试通过,然后继续测试并编写m2

  4. 但如果你没有任何理由存根或嘲笑m2,请不要这样做。一种方法以不需要存根或嘲弄的方式调用其他方法是常见且合理的。被调用的方法可能简短,或者调用方法可能只是分解为一堆辅助方法以便于阅读。如果被调用的方法在另一个类中(因为它被多个调用方法使用),这甚至是正确的;如果它通过调用它的方法的测试进行了全面测试,并且如果测试之间没有重复,则不需要存根或模拟。

    以上模拟m2而不运行m1的示例是一件非常正常的事情,它可以完成任务,但是由于Mockito接管了所有的工作,所以它很丑陋阶级的方法。你可以用Mockito间谍更好地做到这一点,在这里讨论:What is the difference between mocking and spying when using Mockito?

    A a = spy(new A());
    doNothing().when(spy).m2();
    a.m1();
    # check state of a
    

答案 1 :(得分:0)

你仍然应该测试m1()。

你可以使用方法m2()的模拟,

答案 2 :(得分:0)

术语单元测试来自"工作单元"。一个独特的工作单元通常是,但并不总是单一的方法。在实践中,单元测试通常是指测试单个类(比较SRP)并对比集成和验收测试。 JUnit框架来自单元测试,但现在用于所有类型和层次的测试。

作为一个基本的经验法则,您应该测试所有公共方法。如果需要,请添加否定测试,这些测试会使用无效输入提供方法。通常他们会揭示代码的弱点。如果两种公共方法重叠 - 就像你的情况一样 - 我不知道一直使用的原则。你可以

  • A)在从m1()访问期间使用Mockito或类似的模拟m2()。
  • B)测试m1()和m2()使用方法。

A)为您提供对m1代码更敏感的测试,但成本更高。如果m1代码作为单个工作单元很重要,请选择此选项。 B)是更快的结果,在我看来是更好的选择,因为在m1错误的情况下,如果m2中也有错误,你会立即看到。也许为其他解释依赖性的程序员留下一个小小的好评。

答案 3 :(得分:0)

请考虑m1 依赖于m2

现在考虑m1没有调用m2,而是重复m2内部的代码。

(在上述任一方案中,您都会测试m1m2,对吗?)

现在考虑如果重构m1以删除重复代码,通过调用m2会发生什么。由于这是一个真正的重构(即,它不会改变方法的行为),您的现有测试应继续通过。

我的观点是m1m2的依赖是一个私有的实现细节。您通常希望隐藏测试中的实现细节,以防止它们变得脆弱。因此,您应该编写测试,就好像他们不知道这种依赖关系一样。

修改:添加一些代码以进行演示。

想象一下,我们编写了以下代码和测试(对于任何语法错误道歉;我没有编译器方便):

interface Foo {
    public void doStuff(int value);
}

interface Bar {
    public void doOtherStuff();
}

class MyClass {
    private Foo foo;
    private Bar bar;

    public MyClass(Foo foo, Bar bar) {
        this.foo = foo;
        this.bar = bar;
    }

    public void m1() {
        foo.doStuff(42);
        foo.doOtherStuff();
    }

    public void m2() {
        foo.doStuff(42);
    }
}

@Test
public void m1ShouldDoALotOfStuff() throws Exception {
    Foo foo = PowerMockito.mock(Foo.class);
    Bar bar = PowerMockito.mock(Bar.class);
    MyClass sut = new MyClass(foo, bar);

    sut.m1();

    verify(foo).doStuff(42);
    verify(bar).doOtherStuff();
}

@Test
public void m2ShouldJustDoStuff() throws Exception {
    Foo foo = PowerMockito.mock(Foo.class);
    Bar bar = PowerMockito.mock(Bar.class);
    MyClass sut = new MyClass(foo, bar);

    sut.m2();

    verify(foo).doStuff(42);
}

在上面的代码中,我们对m1m2进行了绿色测试。现在我们注意到m1m2中有一些重复的代码:foo.doStuff(42);。所以我们重构m1以摆脱重复:

    public void m1() {
        m2();
        foo.doOtherStuff();
    }

我们的测试在进行此更改后仍为绿色,因为我们没有更改m1的行为;我们只更改了详细信息它执行该行为的方式。为了保持健壮,我们的测试应该测试我们期望SUT做什么而不测试如何它。