主持人注:这里已经有39个答案了(有些已经删除了)。在你发表你的答案之前,考虑一下你是否可以为讨论添加一些有意义的东西。你很可能只是在重复别人已经说过的话。


我偶尔发现自己需要将类中的私有方法设为public,只是为了为它编写一些单元测试。

通常这是因为该方法包含类中其他方法之间共享的逻辑,并且单独测试逻辑更整洁,或者另一个原因可能是我想测试同步线程中使用的逻辑,而不必担心线程问题。

其他人发现他们这样做是因为我不喜欢吗?我个人认为,公开一个方法的好处超过了它在类之外没有提供任何服务的问题……

更新

谢谢大家的回答,似乎引起了大家的兴趣。我认为普遍的共识是测试应该通过公共API进行,因为这是使用类的唯一方式,我非常同意这一点。在我上面提到的几个案例中,我会这样做,这是不常见的情况,我认为这样做的好处是值得的。

然而,我可以看到,每个人都指出它不应该真的发生。再仔细想想,我觉得改变你的代码来适应测试是一个坏主意——毕竟我认为测试在某种程度上是一个支持工具,而改变一个系统来“支持一个支持工具”是明显的坏做法。


当前回答

当您想要测试的某个方法中有复杂的逻辑时,这是一个很好的指标,表明该类违反了单一责任原则。

一个很好的解决方案是:

为原始方法的功能创建一个接口。 在类中实现并测试该接口。 将接口注入到原始类中。

其他回答

You should never ever ever let your tests dictate your code. I'm not speaking about TDD or other DDs I mean, exactly what your asking. Does your app need those methods to be public. If it does then test them. If it does not then then don't make them public just for testing. Same with variables and others. Let your application's needs dictate the code, and let your tests test that the need is met. (Again I don't mean testing first or not I mean changing a classes structure to meet a testing goal).

相反,你应该“考高一点”。测试调用私有方法的方法。但是您的测试应该测试您的应用程序需求,而不是您的“实现决策”。

例如(此处为bod伪代码);

   public int books(int a) {
     return add(a, 2);
   }
   private int add(int a, int b) {
     return a+b;
   } 

没有理由测试“add”,你可以测试“books”。

永远不要让你的测试为你做代码设计决策。测试你是否得到了预期的结果,而不是你如何得到结果。

如果你正在使用c#,你可以让方法是内部的。这样就不会污染公共API。

然后将属性添加到dll

[组装:InternalsVisibleTo(“MyTestAssembly”)]

现在所有的方法都在MyTestAssembly项目中可见。也许不完美,但总比为了测试而将私有方法设为公共要好。

我经常会在类中添加类似validate、verify、check等方法,这样就可以调用它来测试对象的内部状态。

有时这个方法被包装在一个ifdef块中(我主要是用c++写的),这样它就不会被编译发布。但是在发行版中,提供遍历程序对象树检查内容的验证方法通常是有用的。

非常有答案的问题。 IHMO,来自@BlueRaja的精彩回答- Danny Pflughoeft是最好的回答之一。

许多答案建议只测试公共界面,但恕我直言 这是不现实的——如果一个方法只需要5个步骤, 您需要分别测试这五个步骤,而不是一起测试。 这需要测试所有五个方法,它们(除了测试之外) 否则可能是私人的。


最重要的是,我想强调的是,“我们是否应该将私有方法公开以进行单元测试”是一个客观正确答案取决于多个参数的问题。 所以我认为在某些情况下我们不需要这样做,而在其他情况下我们应该这样做。


将私有方法设为公共方法还是将私有方法提取为另一个类(新类或现有类)中的公共方法?

It is rarely the best way. A unit test has to test the behavior of one API method/function. If you test a public method that invokes another public method belonging to the same component, you don't unit test the method. You test multiple public methods at the same time. As a consequence, you may duplicate tests, test fixtures, test assertions, the test maintenance and more generally the application design. As the tests value decreases, they often lose interest for developers that write or maintain them.

To avoid all this duplication, instead of making the private method public method, in many cases a better solution is extracting the private method as a public method in a new or an existing class. It will not create a design defect. It will make the code more meaningful and the class less bloat. Besides, sometimes the private method is a routine/subset of the class while the behavior suits better in a specific structure. At last, it also makes the code more testable and avoid tests duplication. We can indeed prevent tests duplication by unit testing the public method in its own test class and in the test class of the client classes, we have just to mock the dependency.

嘲弄私有方法?

虽然可以通过使用反射或PowerMock等工具来实现,但我认为这通常是绕过设计问题的一种方法。 私有成员不是为向其他类公开而设计的。 测试类是另一个类。所以我们应该对它应用同样的规则。

嘲笑被测试对象的公共方法?

您可能希望将修饰符private更改为public以测试该方法。 然后,为了测试使用这个重构的公共方法的方法,您可能会试图通过使用Mockito(间谍概念)工具来模拟重构的公共方法,但与模拟私有方法类似,我们应该避免模拟被测试的对象。

Mockito.spy()文档说它自己:

创建真实对象的间谍。间谍调用真正的方法,除非他们> >存根。 真正的间谍应该小心地偶尔使用,比如当 处理遗留代码。

根据经验,使用spy()通常会降低测试质量及其可读性。 此外,由于测试对象既是模拟对象又是真实对象,因此更容易出错。 这通常是编写无效验收测试的最佳方法。


下面是我用来决定私有方法应该保持私有还是重构的准则。

情况1)如果一个私有方法只被调用一次,就不要将该方法设为public。 它是单个方法的私有方法。因此,您永远不能复制测试逻辑,因为它只调用一次。

情况2)如果私有方法被多次调用,您应该考虑是否应该将私有方法重构为公共方法。

如何决定?

The private method doesn't produce duplication in the tests. -> Keep the method private as it is. The private method produces duplication in the tests. That is, you need to repeat some tests, to assert the same logic for each test that unit-tests public methods using the private method. -> If the repeated processing may make part of the API provided to clients (no security issue, no internal processing, etc...), extract the private method as a public method in a new class. -> Otherwise, if the repeated processing has not to make part of the API provided to clients (security issue, internal processing, etc...), don't widen the visibility of the private method to public. You may leave it unchanged or move the method in a private package class that will never make part of the API and would be never accessible by clients.


代码示例

这些示例依赖于Java和以下库:JUnit、AssertJ(断言匹配器)和Mockito。 但是我认为整个方法对c#也是有效的。

1)私有方法不会在测试代码中创建重复的例子

下面是一个Computation类,它提供了执行一些计算的方法。 所有公共方法都使用mapToInts()方法。

public class Computation {

    public int add(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] + ints[1];
    }

    public int minus(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] - ints[1];
    }

    public int multiply(String a, String b) {
        int[] ints = mapToInts(a, b);
        return ints[0] * ints[1];
    }

    private int[] mapToInts(String a, String b) {
        return new int[] { Integer.parseInt(a), Integer.parseInt(b) };
    }

}

下面是测试代码:

public class ComputationTest {

    private Computation computation = new Computation();

    @Test
    public void add() throws Exception {
        Assert.assertEquals(7, computation.add("3", "4"));
    }

    @Test
    public void minus() throws Exception {
        Assert.assertEquals(2, computation.minus("5", "3"));
    }

    @Test
    public void multiply() throws Exception {
        Assert.assertEquals(100, computation.multiply("20", "5"));
    }

}

我们可以看到私有方法mapToInts()的调用没有复制测试逻辑。 这是一个中间操作,它不会产生我们需要在测试中断言的特定结果。

2)私有方法在测试代码中创建不必要的重复的例子

下面是一个MessageService类,它提供了创建消息的方法。 所有公共方法都使用createHeader()方法:

public class MessageService {

    public Message createMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

    private Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

下面是测试代码:

import java.time.LocalDate;

import org.assertj.core.api.Assertions;
import org.junit.Test;

import junit.framework.Assert;

public class MessageServiceTest {

    private MessageService messageService = new MessageService();

    @Test
    public void createMessage() throws Exception {
        final String inputMessage = "simple message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), false);
    }

    @Test
    public void createEncryptedMessage() throws Exception {
        final String inputMessage = "encryted message";
        final Credentials inputCredentials = new Credentials("user", "pass");
        Message actualMessage = messageService.createEncryptedMessage(inputMessage, inputCredentials);
        // assertion
        Assert.assertEquals("Aç4B36ddflm1Dkok49d1d9gaz", actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(inputCredentials, 9, LocalDate.now(), true);
    }

    @Test
    public void createAnonymousMessage() throws Exception {
        final String inputMessage = "anonymous message";
        Message actualMessage = messageService.createAnonymousMessage(inputMessage);
        // assertion
        Assert.assertEquals(inputMessage, actualMessage.getMessage());
        Assertions.assertThat(actualMessage.getHeader())
                  .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
                  .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);
    }

}

我们可以看到私有方法createHeader()的调用在测试逻辑中创建了一些重复。 createHeader()确实创建了一个我们需要在测试中断言的特定结果。 我们断言了3倍的头内容,而应该只需要一个断言。

We could also note that the asserting duplication is close between the methods but not necessary the same as the private method has a specific logic : Of course, we could have more differences according to the logic complexity of the private method. Besides, at each time we add a new public method in MessageService that calls createHeader(), we will have to add this assertion. Note also that if createHeader() modifies its behavior, all these tests may also need to be modified. Definitively, it is not a very good design.

重构的步骤

假设我们的情况是createHeader()可以作为API的一部分。 我们将通过将createHeader()的访问修饰符更改为public来重构MessageService类:

public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
    return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
}

我们现在可以测试这个方法的幺正性:

@Test
public void createHeader_with_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = true;
  // action
  Header actualHeader = messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), true);
}

@Test
public void createHeader_with_not_encrypted_message() throws Exception {
  ...
  boolean isEncrypted = false;
  // action
  messageService.createHeader(credentials, message, isEncrypted);
  // assertion
  Assertions.assertThat(actualHeader)
              .extracting(Header::getCredentials, Header::getLength, Header::getDate, Header::isEncryptedMessage)
              .containsExactly(Credentials.anonymous(), 9, LocalDate.now(), false);

}

But what about the tests we write previously for public methods of the class that use createHeader() ? Not many differences. In fact, we are still annoyed as these public methods still need to be tested concerning the returned header value. If we remove these assertions, we may not detect regressions about it. We should be able to naturally isolate this processing but we cannot as the createHeader() method belongs to the tested component. That's why I explained at the beginning of my answer that in most of cases, we should favor the extraction of the private method in another class to the change of the access modifier to public.

我们引入HeaderService:

public class HeaderService {

    public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
        return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
    }

}

我们将createHeader()测试迁移到HeaderServiceTest中。

现在MessageService定义了一个HeaderService依赖:

public class MessageService {

    private HeaderService headerService;

    public MessageService(HeaderService headerService) {
        this.headerService = headerService;
    }

    public Message createMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, false);
        return new Message(header, message);
    }

    public Message createEncryptedMessage(String message, Credentials credentials) {
        Header header = headerService.createHeader(credentials, message, true);
        // specific processing to encrypt
        // ......
        return new Message(header, message);
    }

    public Message createAnonymousMessage(String message) {
        Header header = headerService.createHeader(Credentials.anonymous(), message, false);
        return new Message(header, message);
    }

}

在MessageService测试中,我们不再需要断言每个头值,因为这已经测试过了。 我们只想确保Message.getHeader()返回HeaderService.createHeader()返回的内容。

例如,下面是createMessage()测试的新版本:

@Test
public void createMessage() throws Exception {
    final String inputMessage = "simple message";
    final Credentials inputCredentials = new Credentials("user", "pass");
    final Header fakeHeaderForMock = createFakeHeader();
    Mockito.when(headerService.createHeader(inputCredentials, inputMessage, false))
           .thenReturn(fakeHeaderForMock);
    // action
    Message actualMessage = messageService.createMessage(inputMessage, inputCredentials);
    // assertion
    Assert.assertEquals(inputMessage, actualMessage.getMessage());
    Assert.assertSame(fakeHeaderForMock, actualMessage.getHeader());
}

注意,assertSame()用于比较头部的对象引用,而不是内容。 现在,HeaderService.createHeader()可能会改变其行为并返回不同的值,从MessageService测试的角度来看,这无关紧要。

当您想要测试的某个方法中有复杂的逻辑时,这是一个很好的指标,表明该类违反了单一责任原则。

一个很好的解决方案是:

为原始方法的功能创建一个接口。 在类中实现并测试该接口。 将接口注入到原始类中。