Testing Remove method without a call to Add method

2019-06-15 05:31发布

问题:

I am writing test for a class thats manage trees of Tag objects:

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

As you can see the only mode to set the parent property is via the AddChildTag method and this is exactly what i want, my problem is in unit test: since every test should be atomic, how can i test the RemoveChildTag method?

Only way i see is a call to the add method and later to the remove, but in this way if Add as some errors, even the test of remove will fail, so atomicity is lost.

How can that be done?

EDIT
I removed parent property from Tag object, since i no more use it Some test according to solution using NUnit and 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:

Your unit tests should reflect actual use cases of your class. How will your consumers use RemoveChildTag method? Which makes more sense? Which is how you'd use collection of objects?

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

… or

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

Your consumers will remove objects they previously added. This is your use case, "Remove removes elements previously added" (note that it also produces excellent test method name).

Add and Remove methods are often complementary - you can't test one without the other.



回答2:

There are some ways to test Remove method:

  1. Mocking - mock your data structure, and call remove , so you can test calling the right methods
  2. Inheritance - make children protected. Your test class will inherit from Tag class. Now you can init children member so we can test the remove
  3. Use Add Method

I think option 3 is the best, it's ok using other methods in your unit testing, if Add have some error , more then 1 test will fail - you should be able to understand what you need to fix.

Also, each unit test should test some basic behavior, even if you need to do some preparation before. If those preparation failed, fail the test with the relevant comments

Option 1 - is best when you code is reaching to 3rd party - other server, file system & more. Since you don't want to make those calls in unit test - mock the response.

Option 2 - best I can say , is when you want to test protected / private method, without needing to make all the calls "on the way" that your code does (public method that make many calls that eventually call the method you want to test), since you want to test only a specific logic. It also easy to use this option when your class have some state that you want to test, without needing to write a lot of code to bring your class to this state.



回答3:

You could use an PrivateObject Class to Arrange your object under test

Allows test code to call methods and properties on the code under test that would be inaccessible because they are not public.

EDIT

then you could get full access to wrapped object by PrivateObject.RealType and PrivateObject.Target Properties

EDIT

Anyway, every system which break the segregation and encapsulation of a class make useless the black box approach of Unit Testing in TDD and should be avoided as a poison :)



回答4:

A common approach to unit testing is the Arrange-Act-Assert paradigm.

I would personally have two tests for the remove method, one which is removing a child which was never added, what should happen? Should an exception be thrown?

This would look like:

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

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

Then I would have another test for what should happen when an added child is removed:

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

It may be of interest to note that quite a lot of .NET Remove implementations return a bool result which indicates whether any removal was actually performed. See here.