Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I write generic tests for all implementations of an interface with MSpec?

I have an interface IAudioProcessor with a single method IEnumerable<Sample> Process(IEnumerable<Sample> samples). While it is not a requirement of the interface itself, I want to make sure that all my implementations follow some common rules, like for example:

  1. Use deferred execution
  2. Don't change the input samples

It is not hard to create tests for these, but I would have to copy and paste these tests for each implementation. I would like to avoid that.

I would like to do something like this (note the attribute GenericTest and the type parameter):

[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();
    };
}

Is something like this possible?

like image 750
Daniel Hilgarth Avatar asked Nov 11 '11 17:11

Daniel Hilgarth


1 Answers

I'm pretty sure you're looking for Behaviors (also see this row test with behaviors article). You will define the behaviors that every implementation should satisfy (the It fields) in a special class that share the SUT and supporting fields (as necessary).

[Behaviors]
public class DeferredExecutionProcessor
{
    It should_not_have_enumerated_the_original_samples = () =>
    {
        _.Original.DidNotReceive().GetEnumerator();
        ((IEnumerable)_.Original).DidNotReceive().GetEnumerator();
    };

    protected static Context _; 
}

Each of your implementations need to declare that they behave like this special class. You already had a pretty complicated base class with shared setup and behavior, so I'll use it (I prefer a simpler, more explicit setup).

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 _;
}

Your base class defines the common setup (capturing the context enumeration), behavior (processing with the specific impl via the type parameter), and even declaring the behaviors field (again, thanks to the generic type parameter, this will be run for every concrete).

[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;
}

Additionally, you will get output for each of the It fields for each of the implementing classes. So your context/spec reports will be complete.

like image 110
Anthony Mastrean Avatar answered Oct 16 '22 05:10

Anthony Mastrean