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

时间:2016-08-17 08:50:21

标签: unit-testing junit mocking tdd

我是练习TDD的新手,我在写一个看起来很简单的单元测试时遇到了设计问题。

测试方法有两件事:

  1. 调用将对象A转换为对象B的私有方法
  2. 调用另一个将对象B作为参数传递的私有方法
  3. 这样的事情:

    public void doStuff(A objectA) { B objectB = convertToB(objectA); processB(objectB); }

    现在,我在哪里测试转换是否正确完成? 流行的观点认为使用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()对应于前一示例中的转换方法。我尝试测试的是转换是否正确。

3 个答案:

答案 0 :(得分:3)

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

  

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

如果你想使用TDD方法,

不是你应该问的那个。

  

哪些设计改进可以使此代码可测试?

这是一个更好的问题。要对doStuff(A objectA)进行正确的测试,您需要回到规范:doStuff应该做什么?事实上,它的返回类型是无效的,使得可视化的东西变得更难,但让我们假设它执行以下操作之一:

  1. 与外部系统(文件,数据库,网址等)进行交互
  2. 修改内部系统的状态(变量)
  3. 在第一种情况下,验证可以通过模拟外部系统并验证模拟交互来完成;在第二,我们应该能够直接验证结果。无论如何,你需要确定一个特定的结果(让它称之为C)以及为每种输入A测试它的方法。你的测试应该具有以下结构:

    • doStuff(A1)应该产生结果C1
    • doStuff(A2)应生成结果C2

    结果C总是由对象B决定,而对象B又由对象A决定。因此,如果convertToB()被破坏,则测试结果不应与预期值对应并失败。因此,您的转换方法将由测试涵盖。

    修改

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

    1)首先,Client是一个外部依赖,因此必须模拟正确的单元测试。为此,您需要摆脱静态依赖Client client = Client.create()`并将其替换为构造函数或setter注入。这是一个详细的example haw来做到这一点。

    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.
    

    请注意,您返回的SUCCESSFAILED结果无关紧要,因为它不是EmailSender类的责任,而是Client类的责任,因此它应该是不在EmailSenderTest进行测试。

答案 1 :(得分:1)

这是(未修改的)EmailSender类的完整(和工作)测试集:

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

这两项测试应足以完全涵盖被测试的课程。此外,他们应该能够检测EmailSender实现中可能存在的任何错误。

注意每个测试涵盖两个&#34;业务场景中的一个&#34;:a)电子邮件客户端发送电子邮件,收到&#34;成功&#34;或b)电子邮件的结果发送但电子邮件客户端失败了#34;某种结果。第一个测试还检查发送电子邮件的重要细节。所有这些都取决于被测单元的要求。所述要求之一是与外部Client依赖关系的正确交互,这种依赖关系被模拟。

答案 2 :(得分:0)

如果您已经编写了一个方法,现在您正在尝试测试它,那么您就不会练习TDD了。使用TDD,您首先编写测试。现在你没有遇到难以测试的方法的问题,因为你编写的方法已经有了测试。