如何使用MSpec为接口所有实现编写通用测试?

8
我有一个接口IAudioProcessor,其中包含一个方法IEnumerable<Sample> Process(IEnumerable<Sample> samples)。虽然这并不是接口本身的要求,但我希望确保所有我的实现都遵循一些通用规则,例如:
  1. 使用延迟执行
  2. 不更改输入样本
为此编写测试并不难,但我需要为每个实现复制粘贴这些测试。我想避免这种情况发生。
我想做这样的事情(请注意属性GenericTest和类型参数):
[GenericTest(typeof(AudioProcessorImpl1Factory))]
[GenericTest(typeof(AudioProcessorImpl2Factory))]
[GenericTest(typeof(AudioProcessorImpl3Factory))]
public class when_processed_audio_is_returned<TSutFactory>
    where TSutFactory : ISutFactory<IAudioProcessor>, new()
{
    static IAudioProcessor Sut = new TSutFactory().CreateSut();
    protected static Context _ = new Context();

    Establish context = () => _.Original = Substitute.For<IEnumerable<ISample>>();

    Because of = () => Sut.Process(_.Original);

    It should_not_have_enumerated_the_original_samples = () =>
    {
        _.Original.DidNotReceive().GetEnumerator();
        ((IEnumerable)_.Original).DidNotReceive().GetEnumerator();
    };
}

这是可能的吗?

啊,是的。非常感谢你的投票,却没有解释它的原因。这样我就没有办法改进我的问题了。 - Daniel Hilgarth
1
我也不知道为什么你被踩了,所以我的赞就是为了正义。 - user47589
1
也许我漏掉了什么,但是在我看来,你想要在接口上断言一些不属于接口本身的东西。将那个常见的行为/实现移动到一个实际的具体基础实现中会更容易吧? - Mathias
@Mathias:很好的观点,这也是我通常做的,但我看不出这在这种情况下如何起作用。正如您从样本规范中所看到的,我希望确保只要结果没有被枚举,所有我的实现都不会枚举输入数据。由于实现可以对输入数据进行任何操作,因此无法创建基本实现。 - Daniel Hilgarth
2个回答

2
我非常确定您正在寻找 行为(还请查看此 带有行为文章的行测试)。您将在一个特殊的类中定义每个实现都应满足的行为(It字段),该类共享SUT和支持字段(如有必要)。
[Behaviors]
public class DeferredExecutionProcessor
{
    It should_not_have_enumerated_the_original_samples = () =>
    {
        _.Original.DidNotReceive().GetEnumerator();
        ((IEnumerable)_.Original).DidNotReceive().GetEnumerator();
    };

    protected static Context _; 
}

每个实现都需要声明它们的行为类似于这个特殊的类。您已经有一个非常复杂的基类,其中包含共享的设置和行为,因此我将使用它(我更喜欢更简单、更明确的设置)。
public abstract class AudioProcessorContext<TSutFactory>
    where TSutFactory : ISutFactory<IAudioProcessor>, new()
{
    // I don't know Behaves_like works with field initializers
    Establish context = () => 
    {
        Sut = new TSutFactory().CreateSut();

        _ = new Context();
        _.Original = Substitute.For<IEnumerable<ISample>>();
    }

    protected static IAudioProcessor Sut;
    protected static Context _;
}

你的基类定义了通用的设置(通过上下文枚举进行捕获)、行为(通过特定实现和类型参数进行处理),甚至声明了行为字段(同样由于泛型类型参数,这将针对每个具体实现运行)。
[Subject("Audio Processor Impl 1")]
public class when_impl1_processes_audio : AudioProcessorContext<AudioProcessorImpl1Factory>
{
    Because of = () => Sut.Process(_.Original);
    Behaves_like<DeferredExecutionProcessor> specs;
}

[Subject("Audio Processor Impl 2")]
public class when_impl2_processes_audio : AudioProcessorContext<AudioProcessorImpl2Factory>
{
    Because of = () => Sut.Process(_.Original);
    Behaves_like<DeferredExecutionProcessor> specs;
}

[Subject("Audio Processor Impl 3")]
public class when_impl3_processes_audio : AudioProcessorContext<AudioProcessorImpl3Factory>
{
    Because of = () => Sut.Process(_.Original);
    Behaves_like<DeferredExecutionProcessor> specs;
}

此外,您将获得每个实现类的每个 It 字段的输出。因此,您的上下文/规范报告将是完整的。

不,我不是在寻找行为。你的建议与Alex Norcliffe的Edit 1没有什么区别。它不起作用,因为MSpec不继承ItBehaviors... - Daniel Hilgarth
然后将它们从基本测试类中剥离出来,并放置在每个具体的测试类中。 - Anthony Mastrean
这就是我现在拥有的,但不是我想要的。 - Daniel Hilgarth
那么,你想要的在MSpec中是不可能实现的。你需要向Alexander Gross或Aaron Jensen寻求帮助,以开发新功能(或使ItBehaves_like成为可继承字段)。 - Anthony Mastrean

0

我为你找到了 MSpec 参数化测试的相关内容 :) http://groups.google.com/group/machine_users/browse_thread/thread/8419cde3f07ffcf2?pli=1

虽然它不会显示为单独的绿色/红色测试,但我认为没有什么能阻止你在单个规范中枚举一系列工厂并断言每个实现的行为。这意味着即使一个实现失败,你的测试也会失败,但如果你想要参数化,你可以尝试像 NUnit 这样更宽松的测试套件。

编辑1: 我不确定 MSpec 是否支持发现继承字段以确定规范,但如果是这样,下面的代码至少应该最小化“重复”的代码量,而无法使用属性:

private class base_when_processed_audio_is_returned<TSutFactory>
    where TSutFactory : ISutFactory<IAudioProcessor>, new()
{
    static IAudioProcessor Sut = new TSutFactory().CreateSut();
    protected static Context _ = new Context();

    Establish context = () => _.Original = Substitute.For<IEnumerable<ISample>>();

    public Because of = () => Sut.Process(_.Original);

    public It should_not_have_enumerated_the_original_samples = () =>
    {
        _.Original.DidNotReceive().GetEnumerator();
        ((IEnumerable)_.Original).DidNotReceive().GetEnumerator();
    };
}

public class when_processed_audio_is_returned_from_AudioProcessorImpl1Factory()
  : base_when_processed_audio_is_returned<AudioProcessorImpl1Factory>
{}

public class when_processed_audio_is_returned_from_AudioProcessorImpl2Factory()
  : base_when_processed_audio_is_returned<AudioProcessorImpl2Factory>
{}

我在谷歌上搜到了这个相同的帖子。尽管你肯定可以争论它是一个参数化测试,但我并没有考虑我的情况是这样的。 - Daniel Hilgarth
您提出的一种测试方案涵盖了所有实现,但正如您所言,如果有一个实现是错误的,该测试就会失败,因此我希望避免使用这种方案。 - Daniel Hilgarth
添加了一个编辑,不确定 MSpec 是否可以发现继承的规范细节。 - Alex Norcliffe
不,它没有。那是我尝试的第一件事。它继承了“建立”和“因为”,但没有继承“它”。不过我不确定为什么... - Daniel Hilgarth
好吧,我猜如果有一个字段 It_should_not_have_enumerated_the_original_samples = base.really_should_not_have_enumerated_the_original_samples; 那么这个问题就解决了,但是这样做并没有节省太多的工作量。 - Alex Norcliffe
不是真的。目前,我正在这样做 - 我只是将所有的 It 封装在一个行为中... - Daniel Hilgarth

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