Подтвердить что ты не робот

Единичное тестирование и проверка значения частной переменной

Я пишу модульные тесты с С#, NUnit и Rhino Mocks. Вот соответствующие части класса, который я тестирую:

public class ClassToBeTested
{
    private IList<object> insertItems = new List<object>();

    public bool OnSave(object entity, object id)
    {
        var auditable = entity as IAuditable;
        if (auditable != null) insertItems.Add(entity);

        return false;            
    }
}

Я хочу проверить значения в insertItems после вызова OnSave:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to insertItems array            
}

Какова наилучшая практика для этого? Я рассмотрел добавление insertItems в качестве свойства с публичным get или введением List в ClassToBeTested, но не уверен, что я должен модифицировать код для целей тестирования.

Я прочитал много сообщений о тестировании частных методов и рефакторинга, но это такой простой класс, я задавался вопросом, что является лучшим вариантом.

4b9b3361

Ответ 1

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

Более длинный ответ относится к тому, что делать тогда? В этом случае важно понять, почему реализация такая, как есть (поэтому TDD настолько силен, потому что мы используем тесты для определения ожидаемого поведения, но я чувствую, что вы не используете TDD).

В вашем случае первым вопросом, который приходит на ум, является: "Почему объекты IAuditable добавлены во внутренний список?" или, иначе говоря, "Каков ожидаемый внешне видимый результат этой реализации?" В зависимости от ответа на эти вопросы, это то, что вам нужно проверить.

Если вы добавите объекты IAuditable во внутренний список, потому что позже вы хотите записать их в журнал аудита (просто дикое предположение), затем вызовите метод, который записывает журнал и проверяет, что ожидаемые данные были написаны.

Если вы добавите объект IAuditable во внутренний список, потому что хотите собрать доказательства против какого-то более позднего ограничения, попробуйте проверить это.

Если вы добавили код без каких-либо измеримых причин, затем удалите его еще раз:)

Важная роль заключается в том, что очень полезно тестировать поведение вместо реализации. Это также более надежная и поддерживаемая форма тестирования.

Не бойтесь изменять свой тестовый тест (SUT), чтобы его можно было тестировать более тщательно. Пока ваши дополнения имеют смысл в вашем домене и следуют объектно-ориентированным передовым практикам, проблем нет - вы просто будете следовать принципу Open/Closed.

Ответ 2

Вам не следует проверять список, в который был добавлен элемент. Если вы это сделаете, вы напишете unit test для метода Добавить в списке, а не тест для своего кода. Просто проверьте возвращаемое значение OnSave; что действительно все, что вы хотите проверить.

Если вы действительно обеспокоены добавлением, извините его из уравнения.

Edit:

@TonE: прочитав ваши комментарии, я бы сказал, что вы можете изменить свой текущий метод OnSave, чтобы вы знали о сбоях. Вы можете выбрать исключение, если сбой броска и т.д. Затем вы можете написать unit test, который ожидает и исключение, и тот, который этого не делает.

Ответ 3

Я бы сказал, что "лучшая практика" заключается в том, чтобы проверить что-то значимое с другим, отличным от того, что он сохранил объект в списке.

Другими словами, какое поведение отличается от класса теперь, когда оно его хранит, и проверяйте его. Хранилище представляет собой деталь реализации.

При этом не всегда возможно это сделать.

Вы можете использовать reflection, если это необходимо.

Ответ 4

Если я не ошибаюсь, то, что вы действительно хотите проверить, состоит в том, что он добавляет только элементы в список, когда их можно отличить до IAuditable. Итак, вы можете написать несколько тестов с именами методов, например:

  • NotIAuditableIsNotSaved
  • IAuditableInstanceIsSaved
  • IAuditableSubclassInstanceIsSaved

... и т.д.

Проблема заключается в том, что, как вы заметили, данный код в вашем вопросе, вы можете сделать это только по косвенности - только путем проверки частного члена insertItems IList<object> (путем отражения или путем добавления свойства для единственной цели тестирования ) или ввод списка в класс:

public class ClassToBeTested
{
    private IList _InsertItems = null;

    public ClassToBeTested(IList insertItems) {
      _InsertItems = insertItems;
    }
}

Тогда просто проверить:

[Test]
public void OnSave_Adds_Object_To_InsertItems_Array()
{
     Setup();

     List<object> testList = new List<object>();
     myClassToBeTested     = new MyClassToBeTested(testList);

     // ... create audiableObject here, etc.
     myClassToBeTested.OnSave(auditableObject, null);

     // Check auditableObject has been added to testList
}

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

Это действительно хорошая практика; Это может поразить вас, как немного переусердство, учитывая, что это простой класс, но только его можно проверить. Затем, кроме того, если вы найдете другое место, которое вы хотите использовать в классе, это будет глазурь на торте, поскольку вы не будете ограничены использованием IList (не то, что это потребует больших усилий, чтобы внести изменения позже).

Ответ 5

Если список - это внутренняя деталь реализации (и, похоже, она), то вы не должны ее тестировать.

Хороший вопрос: каково поведение, которое можно было бы ожидать, если бы элемент был добавлен в список? Для этого может потребоваться другой метод.

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        MyOtherClass other = new MyOtherClass();
        c.Save(other);

        var result = c.Retrieve();
        Assert.IsTrue(result.Contains(other));
    }

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

Если результатом является то, что в будущем у объекта с передачей должен быть сделанный вызов в определенных обстоятельствах, тогда у вас может быть что-то вроде этого (прошу простить псевдо-API):

    public void TestMyClass()
    {
        MyClass c = new MyClass();
        IThing other = GetMock();
        c.Save(other);

        c.DoSomething();
        other.AssertWasCalled(o => o.SomeMethod());
    }

В обоих случаях вы проверяете внешне видимое поведение класса, а не внутреннюю реализацию.

Ответ 6

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

Теперь, если вы разрабатываете более крупные тесты, скажем, на уровне интеграции, то нет, вы бы не хотели писать для реализации или тестировать частные методы, но вопрос был направлен на небольшую область unit test.