【问题标题】:What is the difference between a Seam and a Mock?Seam 和 Mock 有什么区别?
【发布时间】:2023-04-04 23:13:02
【问题描述】:

自从我使用 Java 遗留代码以来已经有几个月了,这是我正在处理的一些事情:

  • 0% 测试覆盖率。
  • 在某些场合我什至看到了一些超过 300 行代码的巨大函数。
  • 大量私有方法和静态方法。
  • 高度紧耦合的代码。

一开始我很困惑,发现在 legacy 中很难使用 TDD。在做了几周的 katas 并练习了我的单元测试和模拟技能之后,我的恐惧减少了,我感觉更有信心了。最近我发现了一本书,叫:working effectivelly with legacy,我没有看,只是看了一下目录,我发现了一些对我来说很新的东西,The Seams。显然,这在处理遗留问题时非常重要。

我认为这个 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,模拟如何帮助我?

【问题讨论】:

  • Mocks 不能替代 Seams。
  • 你应该拿到那本书读一读。

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


【解决方案1】:

接缝

A seam 是一个允许你在不修改代码的情况下修改行为的地方。

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

public void doSomeProcessing(ParameterSource request) {..}

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

模拟

现在,您可以使用 Mock 框架来执行类似这样的操作,而不是创建自定义的 fake(创建接口的子类型)

Mocks 还支持断言是否在其上调用了特定方法、参数匹配和其他可供测试使用的漂亮功能。需要维护的测试代码更少。模拟主要用于断言正在对依赖项进行特定调用。在您的示例中,您似乎需要一个存根,您只想返回一个固定值。

请原谅我生锈的 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"});

【讨论】:

  • 我还是有点糊涂。你认为,我上面的例子可以用模拟而不是接缝来测试吗?使用现代框架,我们也可以训练模拟以某些方式表现。另外,我们维护测试,接缝会更难维护吗?
  • 糟糕 - 我的帖子有点混乱。更新了一个模拟示例......可能无法编译 - java 对我来说不是原生的。要使用模拟,您仍然需要一个接缝 - 通常是一个对象接缝作为 ctor 参数或参数。没有“替代”——接缝允许您替换不同的对象(例如,用于测试)。 Mocks 简化了交互测试。它们节省了创建自定义假对象以进行测试和维护的工作。它们还提供更好的报告。
  • 我理解你的解释。感谢您的测试示例很好:) 直到现在我还没有理解单元测试时接缝的重要性。
【解决方案2】:

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

利用接缝的一种方法是插入某种假货。假货可以像您的示例中那样手动滚动,也可以使用 Mockito 之类的工具创建。

因此,mock 是一种假货,假货通常是通过利用 Seam 来使用的。

至于你的测试和你打破依赖的方式,这几乎就是我会做的。

【讨论】:

  • 很好的解释,简短但准确。谢谢。 +1
猜你喜欢
  • 1970-01-01
  • 2011-11-12
  • 2014-06-27
  • 1970-01-01
  • 2010-10-02
  • 1970-01-01
  • 2017-07-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多