什么是Seam和Mock的区别?

15

我已经在使用Java遗留代码工作几个月了,以下是我正在处理的一些问题:

  • 测试覆盖率为0%。
  • 有很多庞大的函数,有时甚至会看到超过300行的代码。
  • 有很多私有方法和静态方法。
  • 代码高度耦合。

一开始我非常困惑,发现在遗留代码中使用TDD很困难。经过数周的kata训练和单元测试以及模拟技能的实践,我的恐惧感降低了,我感觉更加自信了。最近我发现了一本书叫做:有效地处理遗留代码,我没有读过它,只是查看了目录,发现了一些对我来说新的东西,即“接缝”。显然,在处理遗留代码时,这非常重要。

我认为这些接缝可以帮助我打破依赖关系,使我的代码可测试,从而增加代码覆盖率并使我的单元测试更加精确。

但我有很多疑问:

  • 有人能解释一下接缝和模拟之间的区别吗?
  • 在不测试之前,接缝是否违反了TDD规则中不触及生产代码的原则?
  • 你能给我展示一个简单的例子来比较接缝和模拟吗?

下面我想粘贴一个我今天做的例子,试图打破依赖关系,以达到使代码可测试和最终增加测试覆盖率的目的。如果您看到任何错误,请评论一下,谢谢!

这就是遗留代码在开始时的样子:

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%的测试覆盖率。 感谢您的意见:

  • 我是否正确地断开了依赖关系?
  • 单元测试有什么遗漏吗?
  • 有什么可以做得更好的吗?
  • 这个例子足以帮助我理解Seam和Mock之间的区别吗?
  • 如果我不使用Seam,Mock如何帮助我?

Mock 对 Seam 无法替代。 - Gishu
1
你应该买那本书并阅读它。 - Don Roby
2个回答

19

缝隙是代码中可以插入行为修改的地方。当您设置依赖注入时,创建了一个缝隙。

利用缝隙的一种方法是插入某种虚假内容。这些虚假内容可以手工制作,就像在您的示例中一样,也可以使用工具(如Mockito)创建。

因此,模拟是一种虚假类型,并且通常通过利用缝隙来使用虚假。

至于测试和您打破依赖关系的方式,那基本上就是我会做的方式。


很好的解释,简洁而准确。谢谢。+1 - javing

13

接缝

接缝是一个可以在不修改代码的情况下修改行为的地方。

在你的示例中,以下是对象接缝的一个例子(如果我没有弄错的话)。它允许您传入不同的对象而无需更改代码,因此它是一种类型的接缝。

public void doSomeProcessing(ParameterSource request) {..}

通过将参数设置为抽象类型(而不是具体类),您引入了一个接缝。现在,该接缝允许您修改方法的行为而无需编辑其代码 - 即在调用的地方,我可以传递不同的对象并使方法执行其他操作。

模拟

现在,您可以使用模拟框架来替代创建自定义的伪造对象(即创建接口的子类型)以实现以下功能:

模拟还支持断言是否在其上调用了特定方法、参数匹配和其他一些可供测试使用的巧妙功能。需要维护的测试代码更少。模拟主要用于断言对依赖项进行特定调用。在您的示例中,您似乎需要一个存根(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"});

我还有点困惑。你认为,我的上面的例子可以使用模拟代替接缝来进行测试吗?使用现代框架,我们也可以训练模拟对象以特定的方式行为。此外,我们要维护测试,接缝会更难维护吗? - javing
1
翻译:哎呀 - 我的帖子有点混乱。用mocks的示例已经更新...可能无法编译 - Java对我来说不是本地语言。要使用mock,仍然需要一个seam - 通常是一个对象seam作为ctor参数或参数。没有“代替”的概念 - seam允许您替换不同的对象(例如用于测试)。Mocks简化了交互测试。它们节省了为测试和维护创建自定义虚拟对象的工作量。它们还提供更好的报告。 - Gishu
我理解了你的解释。谢谢你提供漂亮的测试例子 :) 直到现在我才意识到在单元测试中,Seams 的重要性。 - javing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接