我是练习TDD的新手,我在写一个看起来很简单的单元测试时遇到了设计问题。
测试方法有两件事:
这样的事情:
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()
对应于前一示例中的转换方法。我尝试测试的是转换是否正确。
答案 0 :(得分:3)
TDD背后的想法是测试功能,而不是方法。所以问题
如果你想使用TDD方法,测试转换方法的正确方法是什么?
不是你应该问的那个。
哪些设计改进可以使此代码可测试?
这是一个更好的问题。要对doStuff(A objectA)
进行正确的测试,您需要回到规范:doStuff
应该做什么?事实上,它的返回类型是无效的,使得可视化的东西变得更难,但让我们假设它执行以下操作之一:
在第一种情况下,验证可以通过模拟外部系统并验证模拟交互来完成;在第二,我们应该能够直接验证结果。无论如何,你需要确定一个特定的结果(让它称之为C)以及为每种输入A测试它的方法。你的测试应该具有以下结构:
结果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.
请注意,您返回的SUCCESS
或FAILED
结果无关紧要,因为它不是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,您首先编写测试。现在你没有遇到难以测试的方法的问题,因为你编写的方法已经有了测试。