简体   繁体   English

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

[英]Should I mock local method invocation inside a tested method?

My question about the concept of unit testing: 我对单元测试概念的疑问:

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

   public void m2() {
      //some code
   }

}

According to best practices, how should I test the m1 method? 根据最佳做法,我应该如何测试m1方法? Is the unit the class or is the unit the method? 单位是类还是单位是方法?

My view - I should test m2 separately and I shouldn't test m1 and m2 integration. 我的观点-我应该分别测试m2 ,而不应该测试m1m2集成。

To use my view is hard enough - I should use sophisticated frameworks for testing and use very modern things. 用我的观点很难-我应该使用复杂的框架进行测试并使用非常现代的东西。

According to my sense of unit testing, tests should be simple! 根据我对单元测试的感觉,测试应该很简单! If your code is good, you can test it without sophisticated things. 如果您的代码是好的,则可以在不复杂的情况下对其进行测试。 But invoking m2() inside m1() is normal code. 但是在m1()中调用m2()是正常代码。

Please clarify my misunderstanding. 请澄清我的误会。

update: 更新:

mocking example(pseudocode): 模拟示例(伪代码):

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

This is how I test a mocked class. 这就是我测试模拟类的方法。 Is this normal? 这正常吗?

First, when you unit-test, test all public methods. 首先,在进行单元测试时,请测试所有公共方法。 In your example m1 and m2 are public, so you'd have tests for both. 在您的示例中, m1m2是公共的,因此您需要对它们进行测试。

There are several reasons that you might want to stub or mock m2 : 您可能想存根或模拟m2原因有几个:

  1. If, when you test m1 , you encounter any problems because m1 calls m2 , stub or mock m2 . 如果在测试m1时遇到任何问题,因为m1调用了m2 ,stub或模拟m2 Some problems you might encounter: 您可能会遇到的一些问题:

    • m2 might call external services m2可能会调用外部服务
    • m2 might just be slow m2可能只是慢
    • it might be difficult to call m1 with parameters that satisfy m2 (your m2 has no parameters, but I'm speaking generally) 可能很难用满足m2参数来调用m1 (您的m2没有参数,但我一般来说)
  2. Sometimes, when you test a method that calls another method and also test the other method, you find that there is duplication between the tests of the two methods -- some of the tests of the calling method are really testing the called method. 有时,当您测试一个调用另一个方法的方法并测试另一个方法时,您会发现这两个方法的测试之间存在重复-调用方法的某些测试实际上是在测试被调用的方法。 Deal with that by stubbing or mocking the called method out of the calling method, testing the calling method just enough to prove that the called method is called, and thoroughly testing the called method. 通过在调用方法中添加或模拟被调用的方法来处理该问题,对调用方法进行足够的测试以证明已调用了该方法,并彻底测试了被调用的方法。

  3. If you do TDD, you might write m1 before you write m2 . 如果执行TDD,则可以在写入m2之前先写入m1 You would then stub or mock m2 so that your tests of m1 would pass, and then go on to test and write m2 . 然后,您将对m2进行存根或模拟,以便对m1的测试通过,然后继续进行测试并编写m2

But if you don't have any reason to stub or mock m2 , don't. 但是,如果您没有任何存根或模拟m2理由,请不要。 It is common and reasonable for a method to call other methods in ways that don't require stubbing or mocking. 一个方法以不需要存根或嘲笑的方式调用其他方法是常见且合理的。 The called methods might be short and simple, or the calling method might just be broken up into a bunch of subsidiary methods for readability. 被调用的方法可能简短而简单,或者出于可读性的考虑,调用方法可能只是分解为一堆辅助方法。 That's even true if the called method is in another class (because it's used by more than one calling method); 如果被调用的方法在另一个类中,则甚至是正确的(因为它被多个调用方法使用); if it is fully tested by tests of methods that call it, and if there is no duplication between tests, there is no need to stub or mock. 如果已通过调用该方法的方法对它进行了全面测试,并且测试之间没有重复,则无需存根或模拟。

The example above of mocking m2 without running m1 is a perfectly normal thing to want to do and it gets the job done, but it's ugly since Mockito takes over all of a class's methods. 上面的示例在不运行m1情况下模拟m2是一件很正常的事情,它可以完成工作,但是这很丑陋,因为Mockito接管了所有类的方法。 You can do it more nicely with a Mockito spy, discussed here: What is the difference between mocking and spying when using Mockito? 您可以使用Mockito间谍程序做得更好,如下所述: 使用Mockito时,模拟和间谍之间有什么区别? .

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

You should still test m1(). 您仍然应该测试m1()。

You could use mocks of method m2(), 您可以使用方法m2()的模拟,

The origin a the term unit test is from "unit of work". 术语“ 单元测试 ”的起源来自“工作单元”。 A distinct unit of work is often, but not always a single method. 一个独特的工作单元通常是但并非总是单个方法。 In practice unit test often refers to testing a single class (compare SRP ) and contrasts integration and acceptance tests. 在实践中,单元测试通常是指测试单个类(比较SRP )并对比集成和验收测试。 The JUnit framework comes from unit testing, but nowadays is used for all kinds and layers of tests. JUnit框架来自单元测试,但如今已用于各种类型的测试层。

As a basic rule of thumb you should test all public methods. 作为基本的经验法则,您应该测试所有公共方法。 If you want, include negative tests, which feed your methods with invalid input. 如果需要,请包括否定测试,这些测试将为您的方法提供无效输入。 Usually they reveal the code's weaknesses. 通常,它们会揭示代码的弱点。 If two public methods overlap - as in your case - I am not aware of a hard use-all-the-time principle. 如果有两种公共方法重叠(例如您的情况),则我不知道严格使用“一直使用”的原则。 You could 你可以

  • A) Use Mockito or similar to mock m2() during access from m1(). A)在从m1()访问期间,使用Mockito或类似的东西模拟m2()。
  • B) Test m1() and m2() the ususal way. B)使用常用方法测试m1()和m2()。

A) gives you a test more sensitive to m1 code but is more costly. A)使您的测试对m1代码更敏感,但代价更高。 Choose this if m1 code is important as a single unit of work. 如果m1代码作为单个工作单元很重要,请选择此选项。 B) is the faster result and in my opinion is the better option as in case of an m1 error you will see immediately if there is an error in m2 as well. B)是更快的结果,我认为是更好的选择,因为如果发生m1错误,那么如果m2也有错误,您将立即看到。 Maybe leave a small nice comment for other programmers that explains the dependence. 也许给其他程序员留下一个很好的注释,以解释这种依赖性。

Consider for a moment that m1 was not dependent on m2 . 考虑一下, m1 依赖于m2

Now consider that m1 didn't invoke m2 , but rather duplicated the code of m2 within itself. 现在考虑m1并没有调用m2 ,而是在其内部复制了m2的代码。

(In either of the above scenarios, you would test both m1 and m2 , right?) (在以上两种情况下,您都将同时测试m1m2 ,对吗?)

Now consider what would happen if you refactored m1 to remove duplicate code, by having it invoke m2 . 现在考虑如果重构m1来删除重复的代码(通过调用m2会发生什么。 Since this is a true refactoring (ie, it does not change the behavior of the method), your existing tests should continue to pass. 由于这是真正的重构(即,它不会改变方法的行为),因此您的现有测试应继续通过。

My point is that the dependence of m1 on m2 is a private implementation detail. 我的观点是m1m2的依赖关系是一个私有的实现细节。 You generally want to hide implementation details from your tests, in order to keep them from becoming brittle. 通常,您想从测试中隐藏实现细节,以防止它们变得脆弱。 Consequently, you should write your tests as if they had no idea about this dependency. 因此,您应该编写测试,就好像他们对这种依赖性一无所知。

Edit: Adding some code to demonstrate. 编辑:添加一些代码来演示。

Imagine we had written the following code and tests (apologies for any syntax errors; I don't have a compiler handy): 假设我们已经编写了以下代码和测试(对于任何语法错误,我们深表歉意;我没有编译器)

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

In the above code we have green tests for both m1 and m2 . 在上面的代码中,我们对m1m2进行了绿色测试。 Now we notice that there is some duplicated code in m1 and m2 : foo.doStuff(42); 现在我们注意到在m1m2中有一些重复的代码: foo.doStuff(42); . So we refactor m1 to get rid of the duplication: 因此,我们重构m1以消除重复:

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

Our tests are still green after making this change, because we haven't changed the behavior of m1 ; 进行此更改后,我们的测试仍为绿色,因为我们尚未更改m1的行为; we've only changed the details of how it carries out that behavior. 我们仅更改了如何执行该行为的细节 In order to be robust, our tests should test what we expect the SUT to do without testing how it does it. 为了稳健,我们的测试应该测试我们所期望的SUT在没有测试它是如何做的事情。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM