Как применить изоляцию теста в MSpec, когда требуются статические члены / методы?

Хорошо. Я пытаюсь понять, почему MSpec использует статические методы / переменные. (Ну, не совсем статические методы, но с делегатами переменных-членов, это практически то же самое).

Это делает невозможным повторное использование контекстов. Это или пройти и убедиться, что все статические переменные сбрасываются вручную. Это не имеет никакого принуждения на тестовой изоляции. Если один тест устанавливает некоторые переменные, а следующий проверяет их, он пройдет, когда не должен.

Это начинает становиться очень раздражающим. То, что я делаю в одном утверждении "потому что", должно просто оставаться там, а не переноситься на любой другой случайный тест только потому, что он использует один и тот же контекст.

Редактировать-

Вопрос в том, как мне "проверить" изоляцию. Например, посмотрите на спецификации ниже, разделяя FooContext, Давайте сделаем дикую догадку, если should_not_throw проходит?

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

2 ответа

Это техническое и историческое ограничение.

  • Вам нужны поля статики для обмена информацией между делегатами (Установить, Потому что, Это, Очистка).
  • MSpec пытается имитировать rspec, поэтому я думаю, что Аарон счел делегатов подходящими и выпустил синтаксис, который вы видите сегодня, еще в 2008 или 2009 году. Этот синтаксис все еще действует сегодня.

Что касается совместного использования контекста / базовых классов контекста: из того, что вы заявляете, кажется, что вы злоупотребляете концепцией. Вы всегда должны инициализировать статические поля в Установлении, чтобы глобальное состояние не стало проблемой. Разделение контекста должно быть хорошо продумано, поэтому, чтобы процитировать вас, это не происходит случайно. Попробуйте использовать вспомогательные методы для сложной настройки и будьте более многословны (я бы сказал, явно) в ведомствах. Это поможет сделать ваши спецификации более читабельными.

И вот. Я хотел бы представить мое (частичное) решение проблемы (о принудительном применении изоляции и настройки). Это и решение проблемы сантехнического кода все одновременно.

Я в основном помещаю контейнер для автоматической блокировки в экземпляр прибора и проверяю, что прибор воссоздается для каждой отдельной спецификации. Если требуется какая-то другая настройка, просто наследуйте или добавляйте в прибор.

(Обратите внимание, что здесь используются карта структуры и карта структуры / контейнер moq / automocking. Я уверен, что для разных контейнеров / макетов все одинаково.)

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

А вот пример использования.

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

По сути, если что-то нужно передать, поместите это в пример самого прибора. Это "навязывает" разделение.... кое-что.

Я все еще предпочитаю Xunit в этом отношении.

Другие вопросы по тегам