Unit testing 在哪里测试私有方法返回的对象?

Unit testing 在哪里测试私有方法返回的对象?,unit-testing,junit,mocking,tdd,Unit Testing,Junit,Mocking,Tdd,我刚开始练习TDD,在编写看似简单的单元测试时,我面临着一个设计问题 被测试的方法做两件事: 调用将对象a转换为对象B的私有方法 调用另一个将对象B作为参数传递的私有方法 大概是这样的: public void doStuff(对象A){ B objectB=convertToB(objectA); 过程B(对象B); } 现在,我在哪里测试转换是否正确完成? 流行的观点认为使用TDD没有必要使用PowerMock或其他库来测试私有方法。我引用JUnit和Mockito书籍中的实用单元测试: 你

我刚开始练习TDD,在编写看似简单的单元测试时,我面临着一个设计问题

被测试的方法做两件事:

  • 调用将对象a转换为对象B的私有方法
  • 调用另一个将对象B作为参数传递的私有方法
  • 大概是这样的:

    public void doStuff(对象A){
    B objectB=convertToB(objectA);
    过程B(对象B);
    }

    现在,我在哪里测试转换是否正确完成? 流行的观点认为使用TDD没有必要使用PowerMock或其他库来测试私有方法。我引用JUnit和Mockito书籍中的实用单元测试:

    你能做的,也许应该做的第一件事就是避免这种情况。怎么用?按照 TDD方法。考虑一下当您首先进行代码测试时,私有方法是如何产生的。答案 也就是说,它们是在重构阶段创建的,这意味着它们的内容完全由测试覆盖 (假设您确实遵循TDD规则,并且仅在测试失败时编写代码)。这样 在某些情况下,“未经测试的私有方法应该以某种方式进行测试”不存在问题,因为 这种方法根本不存在

    我的下一个想法是使用ArgumentCaptor并验证是否使用正确的参数调用了
    processB()。但是,同样,
    processB()
    也是私有的,所以不能这样做

    当然,为了使我的类可测试,可以使用很多技巧—在类字段中保存
    objectB
    ,或者将我的一个私有方法公开。但这将恶化,而不是改善我的设计

    因此,在这种情况下,我的问题是:测试转换方法的正确方法是什么?什么样的设计改进可以使此代码可测试

    编辑:添加真实示例以更好地了解问题:

    public class EmailSender {
    
        public EmailResult send(Email email) {
            MultivaluedMapImpl formData = prepareFormData(email);
            EmailResult emailResult = processEmailRequest(formData);
        }    
    
        private MultivaluedMapImpl prepareFormData(Email email) {
            MultivaluedMapImpl formData = new MultivaluedMapImpl();
            formData.add(FROM_KEY, email.getSender());
            email.getRecipients().stream().forEach((recipient) -> {
                formData.add(TO_KEY, recipient);
            });
            formData.add(SUBJECT_KEY, email.getSubject());
            formData.add(TEXT_KEY, email.getText());
    
            return formData;
        }
    
        private EmailResult processEmailRequest(MultivaluedMapImpl formData ) {
            Client client = Client.create();
            client.addFilter(new HTTPBasicAuthFilter("api", "API_KEY"));
            WebResource webResource = client.resource(API_URL);
            ClientResponse clientResponse = webResource.type(MediaType.APPLICATION_FORM_URLENCODED).
               post(ClientResponse.class, formData);
            String resultString = clientResponse.getStatusInfo().getFamily().toString();
            EmailResult emailResult = resultString.equals("SUCCESSFUL") ? EmailResult.SUCCESS : EmailResult.FAILED;
            return emailResult;
        }
    }
    

    这里,
    prepareFormData()
    对应于前面示例中的转换方法。我试图测试的是转换是否正确。

    TDD背后的想法是测试功能,而不是方法。所以问题来了

    测试转换方法的正确方法是什么

    如果你想使用TDD方法,你不应该问这个问题

    什么样的设计改进可以使此代码可测试

    这是一个更好的问题。要对
    doStuff(对象a)
    进行适当的测试,您需要回到规范:doStuff
    应该做什么?事实上,它的返回类型是void,这使得事情更难可视化,但让我们假设它执行以下操作之一:

  • 与外部系统(文件、数据库、url等)交互
  • 修改内部系统的状态(变量)
  • 在第一种情况下,验证可以通过模拟外部系统和验证模拟交互来完成;第二,我们应该能够直接验证结果。无论如何,您需要确定一个特定的结果(我们称之为C)和一种方法来测试每种输入a。然后,您的测试应该具有以下结构:

    • doStuff(A1)应产生结果C1
    • doStuff(A2)应产生结果C2
    • 等等
    结果C总是由对象B决定,而对象B又由对象A决定。因此,如果
    convertToB()
    被破坏,则测试结果不应与预期值相对应,并且失败。因此,您的转换方法将包含在测试中

    编辑:

    我将使用您提供的真实示例来说明我的观点

    1) 首先,
    Client
    是一个外部依赖项,因此必须对其进行模拟以进行适当的单元测试。为此,您需要去掉静态依赖项Client=Client.create()`并用构造函数或setter注入替换它。这里是一个详细的山楂做这件事

    2) 现在我们可以模拟客户端:

    Client mockClient = Mockito.mock(Client.class, Mockito.RETURN_DEEP_STUBS);
    WebResource mockWebResource = Mockito.mock(WebResource.class);
    Mockito.doReturn(mockWebResource).when(mockClient).resource(Mockito.anyString()); //assuming API_URL is a string
    EmailSender sender = new EmailSender(mockClient);
    
    3) 准备一个具体的测试用例:

    // actual email details 
    Email email = new Email();
    email.setSender("john@domain.com");
    email.setRecipients("chris@domain.com", "bob@domain.com");
    //etc.  
    
    4) 执行测试代码

    sender.send(email);
    
    5) 验证结果

    // capture parameter
    ArgumentCaptor<MultivaluedMapImpl> argument = ArgumentCaptor.forClass(MultivaluedMapImpl.class.class);
    Mockito.verify(mockWebResource, Mockito.times(1)).post(Mockito.any(Class.class), argument.capture());
    Assert.assertEqual(email.getSender(), argument.getValue().get(FROM_KEY);
    Assert.assertEqual(email.getRecipients(), argument.getValue().get(TO_KEY);
    // etc.
    
    //捕获参数
    ArgumentCaptor参数=ArgumentCaptor.forClass(多值MapImpl.class.class);
    Mockito.verify(mockWebResource,Mockito.times(1)).post(Mockito.any(Class.Class),argument.capture());
    Assert.assertEqual(email.getSender(),argument.getValue().get(从_键);
    Assert.assertEqual(email.getRecipients(),argument.getValue().get(TO_键);
    //等等。
    
    请注意,您返回的
    SUCCESS
    FAILED
    结果与此无关,因为它不是
    EmailSender
    类的责任,而是
    Client
    类的责任,因此不应在
    EmailSenderTest
    中对其进行测试。这里是一组完整(且有效)的测试(未修改)
    EmailSender
    class:

    import javax.ws.rs.core.*;
    import com.sun.jersey.api.client.*;
    import com.sun.jersey.api.client.WebResource.Builder;
    import static email.EmailSender.*;
    import mockit.*;
    import static org.junit.Assert.*;
    import org.junit.*;
    
    public class EmailSenderTest {
        @Tested EmailSender emailSender;
        @Mocked Client emailClient;
        @Mocked ClientResponse response;
        Email email;
    
        @Before
        public void createTestEmail() {
            email = new Email();
            email.setSender("john@domain.com");
            email.setRecipients("chris@domain.com", "bob@domain.com");
            email.setSubject("Testing");
            email.setText("Just a test");
        }
    
        @Test
        public void successfullySendEmail() {
            new Expectations() {{
                response.getClientResponseStatus(); result = ClientResponse.Status.OK;
            }};
    
            EmailResult result = emailSender.send(email);
    
            new Verifications() {{
                // Verifies correct API URL and media type:
                Builder bldr = emailClient.resource(API_URL).type(
                    MediaType.APPLICATION_FORM_URLENCODED);
    
                // Verifies correct form data:
                MultivaluedMap<String, String> formData;
                bldr.post(ClientResponse.class, formData = withCapture());
                assertEquals(email.getSender(), formData.getFirst(FROM_KEY));
                assertEquals(email.getRecipients(), formData.get(TO_KEY));
                assertEquals(email.getSubject(), formData.getFirst(SUBJECT_KEY));
                assertEquals(email.getText(), formData.getFirst(TEXT_KEY));
            }};
    
            assertSame(EmailResult.SUCCESS, result);
        }
    
        @Test
        public void failToSendEmail() {
            new Expectations() {{
                response.getClientResponseStatus();
                result = ClientResponse.Status.NOT_FOUND;
            }};
    
            EmailResult result = emailSender.send(email);
    
            // No need to repeat here the verification for URL, form data, etc.
            assertSame(EmailResult.FAILED, result);
        }
    }
    
    导入javax.ws.rs.core.*; 导入com.sun.jersey.api.client.*; 导入com.sun.jersey.api.client.WebResource.Builder; 导入静态电子邮件。EmailSender.*; 输入mockit.*; 导入静态org.junit.Assert.*; 导入org.junit.*; 公共类EmailSenderTest{ @测试邮件发送者; @模拟客户机; @嘲弄的回应; 电子邮件; @以前 public void createTestEmail(){ 电子邮件=新电子邮件(); 电子邮件。setSender(“john@domain.com"); email.setRecipients(“chris@domain.com", "bob@domain.com"); 电子邮件。setSubject(“测试”); setext(“只是一个测试”); } @试验 public void成功发送邮件(){ 新期望(){{ response.getClientResponseStatus();result=ClientResponse.Status.OK; }}; EmailResult结果=emailSender.send(电子邮件); 新的核查(){{ //验证正确的API URL和媒体类型: Builder bldr=emailClient.resource(API_URL).type( MediaType.APPLICATION\u FORM\u URLENCODED); //验证是否正确