Модульное тестирование и проверка значения частной переменной

Я пишу модульные тесты с использованием C #, 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, но не уверен, что мне следует изменять код в целях тестирования.

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


person TonE    schedule 07.07.2009    source источник
comment
Почему OnSave всегда возвращает false? = / Должен ли быть возврат true внутри условия if (auditable! = Null) после .Add? Конечно, это нужно заключить в скобки.   -  person Brent Rittenhouse    schedule 18.06.2018
comment
Этому вопросу почти 9 лет, поэтому я не могу вспомнить наверняка. Возможно, поскольку я использовал TDD, я не дошел до реализации логики возврата метода OnSave, когда разместил этот вопрос.   -  person TonE    schedule 19.06.2018


Ответы (6)


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

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

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

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

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

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

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

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

person Mark Seemann    schedule 07.07.2009
comment
Ваша «дикая догадка» верна. Я изменил свой тест, чтобы вызвать метод OnSave, за которым следует метод, записывающий данные журнала. Затем я утверждаю, что данные были зарегистрированы, не обращая внимания на IList. Если вызов двух методов из SUT в одном тесте является приемлемой практикой, то это будет моим предпочтительным решением. Спасибо. - person TonE; 08.07.2009
comment
Событие в тестах Arrange-Act-Assert или в четырехфазных тестах совершенно законно вызывать более одного члена SUT. Следует действительно стремиться следовать принципу ТЕСТИРОВАНИЯ только одной вещи за раз, но я думаю, что ваш подход хорошо соответствует этому принципу. По сути, вызов метода OnSave - это всего лишь часть фазы Arrange / Setup Fixture. Фаза действия выполняется, когда вы вызываете метод ведения журнала. Никаких нарушений лучших практик нет :) - person Mark Seemann; 08.07.2009
comment
Подобные утверждения (никогда не следует тестировать частные интерфейсы) меня озадачивают. Позвольте мне немного исследовать: возьмем, например, фабрику, которая должна возвращать предоставленные ресурсы в свободный пул, когда все клиенты их освободили. Поскольку нет необходимости раскрывать состояние пула клиентам, он является частным. Как проверить правильность поведения в подобных ситуациях? (часть 1 из 2) - person U007D; 18.10.2016
comment
Я согласен с подходом, который вы предлагаете выше. Но что, если измерять состояние свободного пула нецелесообразно? Разве прагматизм не подтолкнет человека к тестированию внутреннего состояния с полным пониманием того, что, как и в любом случае, когда поведение изменяется, эти тесты потребуют рефакторинга, если кто-то позже реализует другую схему? Фактически, они загорались красным и сообщали разработчику, что было задумано. Я не оспариваю мудрость вашего ответа в целом; Мне искренне интересно, как бы вы порекомендовали разрешить несколько менее распространенную ситуацию, подобную этой. (часть 2 из 2) - person U007D; 18.10.2016
comment
@bRadGibson Если вам что-то небезразлично, как вы проверите, что оно работает так, как задумано? Почему бы не включить проверку? Если вы сделаете API только для чтения для проверки, это будет проблемой? - person Mark Seemann; 18.10.2016
comment
@MarkSeemann Да, это тоже приемлемо. Но идея изменить интерфейс не для клиентов, а исключительно для тестов тоже немного тревожит. В зависимости от конкретной ситуации, запрос к частному интерфейсу или добавление API проверки - разумный компромисс, ИМХО. - person U007D; 18.10.2016
comment
@bRadGibson Я считаю изменение интерфейса SUT совершенно нормальной частью процесса TDD: блог .ploeh.dk / 2011/11/10 / TDDimprovesreusability - person Mark Seemann; 18.10.2016
comment
Хммм ... Модульные тесты - это первый клиент производственного API. Это очень интересная и убедительная перспектива. У меня все еще есть опасения по поводу загрязнения интерфейса чем-то, что не нужно вторым клиентам, но я подумаю об этом более глубоко. Спасибо за ваши мысли - очень признательны! - person U007D; 18.10.2016

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

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

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

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

person Esteban Araya    schedule 07.07.2009
comment
Спасибо - чтобы добавить немного больше контекста, OnSave - это переопределение, которое должно возвращать false, поэтому я не могу использовать здесь возвращаемое значение. Я предполагаю, что я хочу проверить, что вызывается метод Add для списка, а не проверять содержимое списка. Это возвращает меня к издевательству над списком и его введению, или есть какой-то другой способ ... - person TonE; 07.07.2009
comment
Спасибо за предложение, но это ожидаемое условие для вызова OnSave с объектами, которые не реализуют IAuditable - я не заинтересован в создании исключения в этом случае. Я вижу, что во многих ситуациях ваше предложение может помочь вам продвинуться вперед. - person TonE; 08.07.2009

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

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

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

При необходимости вы можете использовать отражение.

person Yishai    schedule 07.07.2009
comment
Единственный способ, которым класс будет вести себя иначе, - это вызов второго метода. Это проверяет список на наличие элементов. Отметьте это поведение, чтобы убедиться, что добавление элемента в список повлечет за собой тестирование двух методов в одном и том же тесте. - person TonE; 07.07.2009
comment
Я не вижу ничего плохого в необходимости вызывать два метода для проведения теста, но я больше сторонник TDD / BDD, чем специалист по тестированию. Я думаю, что альтернатива слишком сильно связывает ваш тест с деталями реализации вашего класса, делая ваши тесты обузой, а не помощником. Но разумные люди в этом не согласны. - person Yishai; 07.07.2009

Если я не ошибаюсь, вы действительно хотите проверить, что он добавляет элементы в список только тогда, когда их можно привести к 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
}

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

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

person Jeff Sternal    schedule 07.07.2009
comment
У меня есть эти три метода тестирования в моем фактическом коде, и, как вы говорите, проблема заключается в том, чтобы проверить, действительно ли элемент добавлен в список, когда возвращаемое значение недоступно для использования. Внедрение списка решает мою проблему, но инъекция будет использоваться только для тестирования - в производстве список будет полностью внутренним по отношению к классу, поскольку это деталь реализации. Допустимо ли добавлять конструктор, вводящий список, исключительно для тестирования? Спасибо! - person TonE; 07.07.2009
comment
Ах, извините за такой педантичный ответ - я не заметил при первом чтении вашего вопроса, что вы уже отлично разбираетесь в проблемах. Я редактирую его, чтобы он был немного менее помпезным и лучше отвечал на ваш основной вопрос. - person Jeff Sternal; 07.07.2009
comment
Инъекция действительно решает мою проблему чисто. Однако я решил избежать каких-либо изменений кода, протестировав поведение других методов в результате вызова OnSave. В моей голове IList все еще является деталью реализации, поэтому я оставил его как таковой. Опять же, во многих сценариях я бы обязательно использовал опцию инъекции. - person TonE; 08.07.2009

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

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

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

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

person kyoryu    schedule 08.07.2009

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

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

person Chris Golledge    schedule 04.09.2015