2013-04-17 3 views
2

Это обсуждалось несколько раз раньше, но достоинства в приведенных ниже примерах не очевидны, поэтому, пожалуйста, несите меня.C# Unit Test - для издевки, заглушки или использования явной реализации

Я пытаюсь решить, использовать ли макетные реализации в своих модульных тестах и ​​не определился с приведенными ниже двумя примерами: первый с использованием NSubstitute для издевательств, а второй с использованием разрешенной реализации SimpleInjector (Bootstrapper object).

По сути, оба тестируют одно и то же, что для объекта Disposed установлено значение true при вызове метода .Dispose() (см. Реализацию метода в нижней части этого сообщения).

На мой взгляд, второй метод имеет большее значение для регрессионного тестирования, поскольку прокси-модуль mock явно устанавливает для параметра Disposed значение true в первом примере, тогда как он задается фактическим методом .Dispose() в внедренной реализации ,

Почему вы предлагаете выбрать один из них для проверки того, что метод ведет себя так, как ожидалось? То есть что вызывается метод .Dispose() и что этот элемент установлен правильно.

[Test] 
    public void Mock_socket_base_dispose_call_is_received() 
    { 
     var socketBase = Substitute.For<ISocketBase>(); 
     socketBase.Disposed.Should().BeFalse("this is the default disposed state."); 

     socketBase.Dispose(); 
     socketBase.Received(1).Dispose(); 

     socketBase.Disposed.Returns(true); 
     socketBase.Disposed.Should().BeTrue("the ISafeDisposable interface requires this."); 
    } 

    [Test] 
    public void Socket_base_is_marked_as_disposed() 
    { 
     var socketBase = Bootstrapper.GetInstance<ISocketBase>(); 
     socketBase.Disposed.Should().BeFalse("this is the default disposed state."); 
     socketBase.Dispose(); 
     socketBase.Disposed.Should().BeTrue("the ISafeDisposable interface requires this."); 
    } 

Для справки метод .Dispose() просто так:

/// <summary> 
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 
    /// </summary> 
    public void Dispose() 
    { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 

    /// <summary> 
    /// Releases unmanaged and - optionally - managed resources. 
    /// </summary> 
    /// <param name="disposeAndFinalize"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> 
    protected void Dispose(bool disposeAndFinalize) 
    { 
     if (Disposed) 
     { 
      return; 
     } 

     if (disposeAndFinalize) 
     { 
      DisposeManagedResources(); 
     } 

     DisposeUnmanagedResources(); 

     Disposed = true; 
    } 

Приветствия

+0

Я говорю, чтобы ваши тесты были максимально простыми. Макет, только если тест достаточно сложный, чтобы потребовать его, ИМХО. Если вы используете VS2012, попробуйте использовать [подделки] (http://msdn.microsoft.com/en-us/library/hh549175.aspx). В вашем случае здесь я не вижу ничего плохого в вашем втором подходе (я не пробовал ваш код, но он выглядит хорошо) –

+1

@GabeThorns: Но если тест «достаточно сложный», может быть, проблема может быть связана с тест (или тестируемый класс) :-) – Steven

+1

Я согласен с @Steven Тесты не должны быть «сложными», поскольку это имеет тенденцию сигнализировать о запахе кода – McDonnellDean

ответ

2

Оба метода испытаний кажутся довольно странными для меня. С помощью первого метода вы, кажется, ничего не тестируете (или, может быть, неправильно понимаете, что делает NSubstitute), потому что вы просто издеваетесь над интерфейсом ISocketBase (который не имеет никакого поведения для тестирования) и начать тестирование этого макета вместо реальной реализации ,

Второй метод плохой, так как вы должны NOT использовать любой контейнер DI внутри ваших модульных тестов. Это только усложняет работу, потому что:

  1. Теперь вы используете общее состояние, в котором используются все тесты, что заставляет все тесты зависеть друг от друга (тесты должны выполняться изолированно).
  2. Логика бутстрапа контейнера будет очень сложной, потому что вы хотите вставлять разные макеты для разных тестов, а также нет объектов, разделяемых между тестами.
  3. Ваши тесты получили дополнительную зависимость от фреймворка или фасада, который просто не существует. В этом смысле вы просто усложняете свои тесты. Это может быть немного сложнее, но тем не менее это дополнительное осложнение.

Вместо того, что вы должны сделать, это всегда создать класс под тест (SUT) внутри модульного тестирования (или заводским способом тест) сам. Вы все равно можете создавать зависимости SUT, используя фальшивую фреймворк, но это необязательно. Таким образом, IMO тест должен выглядеть следующим образом:

[Test] 
public void A_nondisposed_Socket_base_should_not_be_marked_dispose() 
{ 
    // Arrange 
    Socket socket = CreateValidSocket(); 

    // Assert 
    socketBase.Disposed.Should().BeFalse(
     "A non-disposed socket should not be flagged."); 
} 

[Test] 
public void Socket_base_is_marked_as_disposed_after_calling_dispose() 
{ 
    // Arrange 
    Socket socket = CreateValidSocket(); 

    // Act 
    socketBase.Dispose(); 

    // Assert 
    socketBase.Disposed.Should().BeTrue(
     "Should be flagged as Disposed."); 
} 

private static Socket CreateValidSocket() 
{ 
    return new Socket(
     new FakeDependency1(), new FakeDependency2()); 
} 

Обратите внимание, что я разделить свой единственный тест на 2 тестов. То, что Disposed должно быть ложным до вызова утилиты, не является предварительным условием для запуска этого теста; это требование системы работать. Другими словами, вам нужно быть откровенным в этом вопросе и нуждаться в этом втором тесте.

Также обратите внимание на использование фабричного метода CreateValidSocket, который повторно используется для нескольких тестов. У вас может быть несколько перегрузок (или дополнительных параметров) для этого метода, когда другие тесты проверяют другие части класса, которые требуют более конкретных поддельных или макетных объектов.

+0

Альтернатива NSubstitute (которая, по общему признанию, должна быть разделена на два теста в любом случае) просто тестирует, что метод .Dispose() вызывается один раз и что для параметра Disposed установлено значение true. Я не считаю это особенно полезным, поскольку Disposed будет, конечно, равным true, если я прямо его установил сам? – Jimmy

+0

Я не понимаю вашего комментария. Почему вы должны установить «Disposed» в true, когда это результат, который вам нужно проверить? То, что вы хотите проверить, - «Disposed is false, когда Dispose() НЕ вызывается» и «Disposed is true, когда Dispose IS вызывается». – Steven

+0

Ваши утверждения NOT и ALWAY просто стилистичны. Вы также утверждаете, что контейнер DI - это нечто большее, чем способ запроса зависимостей, поэтому нет необходимости использовать структуру DI, если вы хотите использовать контейнер DI. – McDonnellDean

1

Вы обеспокоены слишком много. Этот тест проверяет погоду или не выполняет определенную реализацию правильно, и поэтому ваш тест должен отражать это. См. Псевдо-код ниже. Трюк для не хрупких тестов - это только проверка абсолютного минимума, необходимого для проведения теста.

public class When_disposed_is_called() 
{ 
    public void The_object_should_be_disposed() 
    { 
     var disposableObjects = someContainer.GetAll<IDisposable>(); 
     disposableObjects.ForEach(obj => obj.Dispose()); 
     Assert.False(disposableObject.Any(obj => obj.IsDisposed == false)); 
    } 
} 

Как вы можете видеть, что я заполнить некоторые контейнер зависимостей со всеми объектами в моей озабоченности, которые реализуют IDisposable. Мне, возможно, придется издеваться над ними или делать другие вещи, но это не проблема теста. В конечном итоге это касается только проверки того, что когда что-то находится, оно должно быть фактически удалено.