Как применить изоляцию теста в 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 в этом отношении.