自从我使用java遗留代码几个月后,这是我正在处理的一些事情:
一开始我很困惑,我发现很难在传统中使用TDD。在做了几周的katas并练习我的单元测试和嘲讽技巧之后,我的恐惧感降低了,我感到更加自信。最近我发现了一本名为working effectivelly with legacy的书,我没看过,我只是看了一下目录,发现了一些对我来说很新的东西,The Seams。显然,这在遗产工作中非常重要。
我认为这个Seams可以帮助我打破依赖关系并使我的代码可以测试,这样我就可以增加代码覆盖率并使我的单元测试更精确。
但我有很多疑问:
下面我想粘贴一个我今天做的例子,我试图打破一个依赖,目的是使代码可以测试,最后增加测试覆盖率。如果你看到一些错误,如果你能评论一下,我将不胜感激?
这就是遗留代码在开始时的样子:
public class ABitOfLegacy
{
private String sampleTitle;
String output;
public void doSomeProcessing(HttpServletRequest request) {
String [] values = request.getParameterValues(sampleTitle);
if (values != null && values.length > 0)
{
output = sampleTitle + new Date().toString() + values[0];
}
}
}
如果我只是添加一个调用该方法并断言该变量输出的单元测试,在调用后有一定的值,那么我会犯一个错误,因为我不是单元测试,我会做集成测试。所以我需要做的是,摆脱我在参数中的依赖。为此,我用接口替换参数:
public class ABitOfLegacy
{
private String sampleTitle;
String output;
public void doSomeProcessing(ParameterSource request) {
String [] values = request.getParameters(sampleTitle);
if (values != null && values.length > 0)
{
output = sampleTitle + new Date().toString() + values[0];
}
}
}
界面如下所示:
public interface ParameterSource {
String[] getParameters(String name);
}
我接下来要做的是创建我自己的接口实现,但我将HttpServletRequest包含为全局变量,并使用HttpServletRequest的方法实现接口的方法:
public class HttpServletRequestParameterSource implements ParameterSource {
private HttpServletRequest request;
public HttpServletRequestParameterSource(HttpServletRequest request) {
this.request = request;
}
public String[] getParameters(String name) {
return request.getParameterValues(name);
}
}
在此之前,我认为对生产代码的所有修改都是安全的。 现在我在我的测试包中创建了Seam。如果我理解得很好,现在我能够安全地改变Seam的行为。我就是这样做的:
public class FakeParameterSource implements ParameterSource {
public String[] values = {"ParamA","ParamB","ParamC"};
public String[] getParameters(String name) {
return values;
}
}
最后一步是获得Seam的支持,以测试该方法的原始行为。
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import code.ABitOfLegacyRefactored;
import static org.hamcrest.Matchers.*;
public class ABitOfLegacySpecification {
private ABitOfLegacy aBitOfLegacy;
private String EMPTY = null;
@Before
public void initialize() {
aBitOfLegacy = new ABitOfLegacy();
}
@Test
public void
the_output_gets_populated_when_the_request_is_not_empty
() {
FakeParameterSource fakeParameterSource = new FakeParameterSource();
aBitOfLegacy.doSomeProcessing(fakeParameterSource);
assertThat(aBitOfLegacy.output,not(EMPTY));
}
@Test(expected=NullPointerException.class)
public void
should_throw_an_exception_if_the_request_is_null
() {
aBitOfLegacy.doSomeProcessing(null);
}
}
这将给我100%的测试覆盖率。 我很感激你的想法:
答案 0 :(得分:9)
接缝是代码中的一个位置,您可以在行为中插入修改。在设置注入依赖项时创建了一个接缝。
利用接缝的一种方法是插入某种假货。假的可以像你的例子那样手工卷制,或者用Mockito之类的工具创建。
所以,模拟是一种假的,通常利用Seam来使用假的。
至于你的测试以及你打破依赖的方式,这几乎就是我的做法。
答案 1 :(得分:7)
<强>煤层强>
A seam是一个允许您修改行为而无需修改代码的地方。
在您的示例中,以下是Object seam的示例(如果我没有记错的话)。它允许您传递不同的对象而无需更改代码。因此它是一种接缝。
public void doSomeProcessing(ParameterSource request) {..}
通过使参数成为抽象类型(而不是具体类),您已经引入了一个接缝。接缝现在允许您修改方法的行为而无需编辑其代码 - 即在调用的地方,我可以传入不同的对象并使该方法执行其他操作。
<强>嘲笑强>
现在不是创建自定义假(创建接口的子类型),而是可以使用Mock框架来执行此类操作
Mocks还支持断言是否调用了特定方法,参数匹配以及测试要使用的其他漂亮功能。减少要维护的测试代码。模拟主要用于断言对依赖项进行特定调用。在您的示例中,您似乎需要Stub,您只想返回一个预设值。
原谅我生锈的JMock ..
@Test
public void
the_output_does_not_get_populated_when_the_request_is_empty
() {
Mockery context = new Mockery();
final ParameterSource mockSource = context.mock(ParameterSource.class)
context.checking(new Expectations(){{
oneOf(mockSource).getParameters();
will(returnValue(new string[]{"ParamA","ParamB","ParamC"} );
}});
aBitOfLegacy.populate(mockSource);
assertThat(aBitOfLegacy.output,not(EMPTY));
}
<。> .Net
var mockSource = new Mock<ParameterSource>();
mockSource.Setup(src => src.GetParameters())
.Returns(new []{"ParamA","ParamB","ParamC"});