How do I enforce test isolation in MSpec when static members/methods are required?

回眸只為那壹抹淺笑 提交于 2019-12-12 12:10:25

问题


Ok. I'm trying to wrap my head around why MSpec uses static methods / variables. (Well not exactly static methods, but with member variable delegates, it's practically the same).

This makes it impossible to reuse contexts. That or go through and make sure all static variables are reset manually. This has no enforcement on test isolation. If one test sets up some variables and the next one checks for it, it'd pass when it shouldn't.

This is starting to get very annoying. What I do in one "because" statement should just stay there, not get carried through to every other random test just because it's sharing the same context.

Edit-

The question is, how do I "ENFORCE" test isolation. For example, look at the specs below, sharing the FooContext. Let's take a wild guess if should_not_throw passes?

public class FooContext
{
    Establish context = () => Subject = new Foo();

    public static Foo Subject;
    public static int result;
    public static Exception ex;
}

public class When_getting_an_int_incorrectly : FooContext
{
    Because of = () => ex = Exception.Catch(() => result = Subject.GetInt(null)); 

    It should_throw = () => ex.ShouldNotBeNull();
}

public class When_getting_an_int_correctly : FooContext
{
    Because of = () => ex = Exception.Catch(() => result = Subject.GetInt(0));

    It should_not_throw = () => ex.ShouldBeNull();
}

回答1:


It's a technical and a historic limitation.

  • You need statics fields to share information between the delegates (Establish, Because, It, Cleanup).
  • MSpec tries to mimic rspec, so I think Aaron considered delegates to be a good fit and released the syntax you see today back in 2008 or 2009. This syntax is still in place today.

As for context sharing / context base classes: From what you state it seems like you're overusing the concept. You should always initialize static fields in the Establish, so it the global state will become a non-issue. Context sharing should be well considered, so, to quote you, it doesn't happen randomly. Try using helper methods for complex setup and be more verbose (I'd say explicit) in the Establishs. It will help make your specs more readable.




回答2:


Lo and behold. I would like to present my (partial) solution to the problem (Of enforcing fixture and setup isolation). That and resolving the problem of plumbing code all at the same time.

I basically put an automocking container in an instance of the fixture and make sure that fixture is recreated for every single spec. If some other setup is required, just inherit or add to the fixture.

(Note this uses structure map and structure map / moq / automocking container. I'm sure it's all the same for different container/mocking framework.)

/// <summary>
/// This is a base class for all the specs. Note this spec is NOT thread safe. (But then
/// I don't see MSpec running parallel tests anyway)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <remarks>
/// This class provides setup of a fixture which contains a) access to class under test
/// b) an auto mocking container and c) enforce a clean fixture for every spec.
/// </remarks>
public abstract class BaseSpec<T>
    where T : class
{
    public static TestFixture Fixture;
    private Establish a_new_context = () =>
        {
            Fixture = new TestFixture();
            MockedTypes = new Dictionary<Type, Action>();
        };

    /// <summary>
    /// This dictionary holds a list of mocks that need to be verified by the behavior.
    /// </summary>
    private static Dictionary<Type, Action> MockedTypes;
    /// <summary>
    /// Gets the mock of a requested type, and it creates a verify method that is used
    /// in the "AllMocksVerified" behavior.
    /// </summary>
    /// <typeparam name="TMock"></typeparam>
    /// <returns></returns>
    public static Mock<TMock> GetMock<TMock>()
        where TMock : class
    {
        var mock = Mock.Get(Fixture.Context.Get<TMock>());

        if (!MockedTypes.ContainsKey(typeof(TMock)))
            MockedTypes.Add(typeof(TMock), mock.VerifyAll);

        return mock;
    }

    [Behaviors]
    public class AllMocksVerified
    {
        private Machine.Specifications.It should_verify_all =
        () =>
        {
            foreach (var mockedType in MockedTypes)
            {
                mockedType.Value();
            }
        };
    }

    public class TestFixture
    {
        public MoqAutoMocker<T> Context { get; private set; }

        public T TestTarget
        {
            get { return Context.ClassUnderTest; }
        }

        public TestFixture()
        {
            Context = new MoqAutoMocker<T>();
        }
    }
}

And here is a sample usage.

    public class get_existing_goo : BaseSpec<ClassToTest>
    {
        private static readonly Goo Param = new Goo();

        private Establish goo_exist =
            () => GetMock<Foo>()
                      .Setup(a => a.MockMethod())
                      .Returns(Param);

        private static Goo result;

        private Because goo_is_retrieved =
            () => result = Fixture.Context.ClassUnderTest.MethodToTest();

        private It should_not_be_null =
            () => result.ShouldEqual(Param);
    }

Basically if something needs to be shared, put it in the instance of fixture itself. This "enforces" separation.... some what.

I still prefer Xunit in this regard.



来源:https://stackoverflow.com/questions/18139073/how-do-i-enforce-test-isolation-in-mspec-when-static-members-methods-are-require

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!