Тестирование Метод удаления без вызова метода Добавить

Я пишу тест для класса, который управляет деревьями объектов Tag:

public class Tag
{
    public virtual int Id { get; set; }
    public virtual string Description{ get; set; }
    private IList<Tag> children = new List<Tag>();
    public virtual IEnumerable<Tag> Children
    {
        get {return children .ToArray();}
    }
    public void AddChildTag(Tag child)
    {
        children.Add(child);
    }
    public void RemoveChildTag(Tag child)
    {
        children.Remove(child);
    }
}

Как вы можете видеть, единственный способ установить родительское свойство - это метод AddChildTag, и это именно то, что я хочу, моя проблема в unit test: поскольку каждый тест должен быть атомарным, как я могу проверить RemoveChildTag метод?

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

Как это можно сделать?

ИЗМЕНИТЬ
Я удалил родительское свойство из объекта Tag, так как я больше не использую его Некоторые испытания в соответствии с решением с использованием NUnit и FluentAssertion

    [Test]
    public void AddChildTagAddsChildren()
    {
        //Arrange
        Tag parent = new Tag();
        Tag child = new Tag();
        //Act
        parent.AddChildTag(child);
        //Assert
        parent.Children.Should().Contain(child);
    }
    [Test]
    public void RemoveChildTagRemovesAddedChildren()
    {
        //Arrange
        Tag parent = new Tag();
        Tag child = new Tag();
        parent.AddChildTag(child);
        //Act
        parent.RemoveChildTag(child);
        //Assert
        parent.Children.Should().NotContain(child);
    }
    [Test]
    public void RemoveChildTagThrowsNothingWhenNoChild()
    {
        //Arrange
        Tag parent= new Tag();
        Tag child= new Tag();
        //Act
        Action RemoveChild = () => parent.RemoveChildTag(child);
        //Assert
        RemoveChild.ShouldNotThrow();
    }

Ответ 1

В ваших модульных тестах должны отражаться фактические варианты использования вашего класса. Как ваши потребители будут использовать метод RemoveChildTag? Что имеет смысл? Как вы используете коллекцию объектов?

var parent = new Tag();
// later
parent.RemoveChildTag(child);

... или

var parent = new Tag();
parent.AddChildTag(child);
// later
parent.RemoveChildTag(child);

Ваши потребители удалят объекты, которые они ранее добавили. Это ваш прецедент, "Удалить удаляет ранее добавленные элементы" (обратите внимание, что он также дает отличное имя метода тестирования).

Add и Remove методы часто дополняют друг друга - вы не можете тестировать один без другого.

Ответ 2

Есть несколько способов протестировать метод Remove:

  • Издевательский - издевается над вашей структурой данных и удаляет вызов, поэтому вы можете протестировать вызов правильных методов.
  • Наследование - сделать children защищенным. Ваш тестовый класс наследует класс Tag. Теперь вы можете включить элемент children, чтобы мы могли протестировать удаление
  • Использовать метод Add

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

Кроме того, каждый unit test должен протестировать некоторое базовое поведение, даже если вам нужно сделать некоторую подготовку раньше. Если эта подготовка завершилась неудачно, пропустите тест с соответствующими комментариями

Вариант 1 - лучше всего, когда код доходит до третьей стороны - другого сервера, файловой системы и т.д. Поскольку вы не хотите, чтобы эти вызовы в unit test - высмеивали ответ.

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

Ответ 3

Вы можете использовать PrivateObject Class для размещения объекта под тестом

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

ИЗМЕНИТЬ

то вы можете получить полный доступ к обернутому объекту PrivateObject.RealType и PrivateObject.Target Свойства

ИЗМЕНИТЬ

В любом случае, каждая система, которая нарушает сегрегацию и инкапсуляцию класса, делает бесполезным подход Black Unit к модульному тестированию в TDD и его следует избегать как яд:)

Ответ 4

Общим подходом к единичному тестированию является парадигма Arrange-Act-Assert.

У меня было бы лично два теста для метода remove, который удаляет дочерний элемент, который никогда не добавлялся, что должно произойти? Следует ли исключить исключение?

Это будет выглядеть так:

[Test]
public void RemoveChildTagThrowsExceptionWhenNoChildren()
{
    // Arrange
    var tag = new Tag();
    var tagToRemove = new Tag();

    // Act & Assert
    Expect(() => tag.RemoveChildTag(tagToRemove), Throws.ArgumentException);
}

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

[Test]
public void RemoveChildTagRemovesPreviouslyAddedChild()
{
    // Arrange
    var tag = new Tag();
    var childTag = new Tag();
    tag.AddChildTag(childTag);

    // Act
    tag.RemoveChildTag(childTag);

    // Assert
    Expect(tag.Children.Contains(childTag), Is.False);
}

Может показаться интересным отметить, что довольно много реализаций .NET Remove возвращает результат bool, который указывает, действительно ли выполнено удаление. См. здесь.