Seam和Mock有什么区别?

时间:2013-03-10 21:16:17

标签: java unit-testing tdd code-coverage legacy

自从我使用java遗留代码几个月后,这是我正在处理的一些事情:

  • 0%测试覆盖率。
  • 在我甚至看到一些有超过300行代码的情况下的巨大功能。
  • 许多私有方法和静态方法。
  • 高度紧密耦合的代码。

一开始我很困惑,我发现很难在传统中使用TDD。在做了几周的katas并练习我的单元测试和嘲讽技巧之后,我的恐惧感降低了,我感到更加自信。最近我发现了一本名为working effectivelly with legacy的书,我没看过,我只是看了一下目录,发现了一些对我来说很新的东西,The Seams。显然,这在遗产工作中非常重要。

我认为这个Seams可以帮助我打破依赖关系并使我的代码可以测试,这样我就可以增加代码覆盖率并使我的单元测试更精确。

但我有很多疑问:

  • 有人可以解释一下接缝和模拟之间的区别吗?
  • 在测试之前,做什么接缝,在不涉及生产代码的情况下打破TDD规则?
  • 你能告诉我一些比较Seam和Mock的简单例子吗?

下面我想粘贴一个我今天做的例子,我试图打破一个依赖,目的是使代码可以测试,最后增加测试覆盖率。如果你看到一些错误,如果你能评论一下,我将不胜感激?

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

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,模拟怎么能帮助我?

2 个答案:

答案 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"});