C# 将私有方法公开以进行单元测试…好主意?
主持人注意:这里已经发布了39个答案(有些已被删除)。在你发表你的答案之前,考虑一下你是否可以在讨论中添加一些有意义的东西。你很可能只是重复别人已经说过的话C# 将私有方法公开以进行单元测试…好主意?,c#,java,unit-testing,C#,Java,Unit Testing,主持人注意:这里已经发布了39个答案(有些已被删除)。在你发表你的答案之前,考虑一下你是否可以在讨论中添加一些有意义的东西。你很可能只是重复别人已经说过的话 我偶尔会发现自己需要在类中公开一个私有方法,以便为它编写一些单元测试 通常这是因为该方法包含类中其他方法之间共享的逻辑,并且它自己测试逻辑更整洁,或者另一个原因可能是我想测试同步线程中使用的逻辑,而不必担心线程问题 其他人会因为我不喜欢这样做而发现自己在这样做吗??我个人认为奖金超过了公开一个方法的问题,这个方法实际上不提供任何课外服务
我偶尔会发现自己需要在类中公开一个私有方法,以便为它编写一些单元测试 通常这是因为该方法包含类中其他方法之间共享的逻辑,并且它自己测试逻辑更整洁,或者另一个原因可能是我想测试同步线程中使用的逻辑,而不必担心线程问题 其他人会因为我不喜欢这样做而发现自己在这样做吗??我个人认为奖金超过了公开一个方法的问题,这个方法实际上不提供任何课外服务 更新 谢谢大家的回答,似乎激起了人们的兴趣。我认为普遍的共识是测试应该通过公共API进行,因为这是使用类的唯一方式,我同意这一点。我在上面提到的两个案例中,我会这样做,这是不常见的案例,我认为这样做的好处是值得的
然而,我可以看到每个人都认为这永远不会发生。再仔细考虑一下,我认为更改代码以适应测试是一个坏主意——毕竟,我认为测试在某种程度上是一种支持工具,如果愿意的话,将系统更改为“支持支持工具”是明显的坏做法。就我个人而言,我更愿意使用公共API进行单元测试,而且我肯定不会仅仅为了便于测试而将私有方法公开 如果您真的想单独测试私有方法,那么在Java中可以使用/来实现这一点 你必须实事求是,你也应该知道为什么事情很难测试 ''-如果它很难测试,这是否告诉你一些关于你的设计?您是否可以重构到这种方法的测试非常简单,并且很容易通过公共api进行测试 以下是迈克尔·费瑟在《我的爱》中的话 “很多人花了很多时间想办法解决这个问题。。。真正的答案是,如果你有测试私有方法的冲动,那么这个方法不应该是私有的;如果公开这种方法让你感到困扰,很可能是因为它是一种独立责任的一部分;它应该在另一个类上
我倾向于同意,对it进行单元测试的好处大于增加某些成员可见性的问题。一个小小的改进是使其受到保护和虚拟,然后在测试类中重写它以公开它
或者,如果您想单独测试它的功能,它是否表明您的设计中缺少了一个对象?也许您可以将它放在一个单独的可测试类中…然后您现有的类只是委托给这个新类的一个实例。我认为这是一个坏主意,因为我不确定您是否获得了任何好处和潜在的好处如果你只是为了测试一个私有方法而改变一个调用的契约,你不是在测试这个类如何使用它,而是在创建一个你从未想过会发生的人工场景
此外,通过将方法声明为公共的,在六个月后(忘记了公开方法的唯一原因是为了测试)你(或者如果你已经移交了项目)会说什么完全不同的人不会使用它,这会导致潜在的意外后果和/或维护噩梦。单元测试应该测试公共契约,这是在代码的其他部分中使用类的唯一方法。私有方法是实现细节,您不应该测试它;只要公共API工作正常,就不应该测试它实现并不重要,可以在不改变测试用例的情况下进行更改。将其包私有化如何?这样您的测试代码就可以看到它(以及包中的其他类),但它仍然对用户隐藏 但实际上,您不应该测试私有方法。这些是实现细节,而不是合同的一部分。它们所做的一切都应该通过调用公共方法来实现(如果它们的代码不是由公共方法执行的,那么应该这样做)。如果私有代码太复杂,则该类可能做了太多事情,需要重构
公开一个方法是一项巨大的承诺。一旦你这样做了,人们就可以使用它,你就不能再改变它们了。在java中,还可以选择将其包私有化(即取消可见性修饰符)。如果您的单元测试与被测试的类位于同一个包中,那么它应该能够看到这些方法,并且比将该方法完全公开要安全一些。实际上,在某些情况下您应该这样做(例如,当您实现一些复杂的算法时)。只需将其设置为包私有,这就足够了。
但在大多数情况下,您可能有太复杂的类,需要将逻辑分解到其他类中。我通常将测试类与被测试的类保留在同一个项目/程序集中。
这样,我只需要
内部可见性就可以使函数/类可测试
这会使构建过程有些复杂,需要过滤掉测试类。
我通过命名所有测试类TestedClassTest
并使用正则表达式过滤这些类来实现这一点
当然,这只适用于您的应用程序的C#/.NET部分
public class A {
private void doSomething() {
System.out.println("Doing something private.");
}
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class B {
public static final void main(final String[] args) {
try {
Method doSomething = A.class.getDeclaredMethod("doSomething");
A o = new A();
//o.doSomething(); // Compile-time error!
doSomething.setAccessible(true); // If this is not done, you get an IllegalAccessException!
doSomething.invoke(o);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
}
}
public class SystemUnderTest
{
public void DoStuff()
{
// Blah
// Call Validate()
}
private void Validate()
{
// Several lines of complex code...
}
}
public class SystemUnderTest
{
public void DoStuff()
{
// Blah
validator.Invoke(..)
}
}
public class NavigationTest {
private Navigation nav;
@Before
public void setUp() {
// Set up nav so the order is page1->page2->page3 and
// we've moved back to page2
nav = ...;
}
@Test
public void testFirst() {
nav.first();
assertEquals("page1", nav.getPage());
nav.next();
assertEquals("page2", nav.getPage());
nav.next();
assertEquals("page3", nav.getPage());
}
@Test
public void testLast() {
nav.last();
assertEquals("page3", nav.getPage());
nav.previous();
assertEquals("page2", nav.getPage());
nav.previous();
assertEquals("page1", nav.getPage());
}
}
public int books(int a) {
return add(a, 2);
}
private int add(int a, int b) {
return a+b;
}
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"));
}
}
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);
}
}
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);
}
public class HeaderService {
public Header createHeader(Credentials credentials, String message, boolean isEncrypted) {
return new Header(credentials, message.length(), LocalDate.now(), isEncrypted);
}
}
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);
}
}
@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());
}