【问题标题】:Testing Remove method without a call to Add method在不调用 Add 方法的情况下测试 Remove 方法
【发布时间】:2014-06-23 09:21:39
【问题描述】:

我正在为一个管理标签对象树的类编写测试:

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 方法,这正是我想要的,我的问题在于单元测试:因为每个测试都应该是原子的,我如何测试 @ 987654323@方法?

我看到的唯一方法是调用 add 方法,然后调用 remove,但是这样如果 Add 作为一些错误,即使 remove 测试也会失败,因此原子性丢失。

如何做到这一点?

编辑
我从 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();
    }

【问题讨论】:

  • 轻微的旁注:在不调用Add 的情况下调用Remove 的行为也值得测试,以测试是否没有抛出异常或以正确的状态抛出正确的异常。
  • 因为 remove 依赖于add,如果测试通过了你就知道if add works, then remove works。一旦add 被测试,你就会知道remove 有效。
  • @Jonny,请看我对 Lukazoid 答案的评论
  • 问题更新了一些测试
  • 您的测试看起来不错。对于 Fluent,我建议为 act 操作变量提供更多上下文名称(act.ShouldNotThrow()removeChildTag.ShouldNotThrow())。

标签: c# unit-testing tdd


【解决方案1】:

您的单元测试应该反映您班级的实际用例。您的消费者将如何使用RemoveChildTag 方法?哪个更有意义?您将如何使用对象集合?

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

… 或

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

您的消费者将删除他们之前添加的对象。这是您的用例,“Remove 删除之前添加的元素”(请注意,它还会生成出色的测试方法名称)。

AddRemove 方法通常是互补的 - 你不能没有另一个测试一个。

【讨论】:

  • @gt.guybrush 我认为只有这个 aswer 才是正确的答案,为了测试目的而打破封装不是正确的方法
【解决方案2】:

有一些方法可以测试Remove方法:

  1. Mocking - 模拟您的数据结构,并调用 remove ,以便您可以测试调用正确的方法
  2. 继承 - 使 children 受保护。您的测试类将继承自 Tag 类。现在您可以初始化children 成员,以便我们测试删除
  3. 使用Add方法

我认为选项 3 是最好的,可以在单元测试中使用其他方法,如果 Add 有一些错误,则超过 1 次测试将失败 - 你应该能够理解你需要修复什么。

此外,每个单元测试都应该测试一些基本行为,即使您之前需要做一些准备。 如果这些准备失败,则与相关 cmets 测试失败

选项 1 - 当您的代码到达第 3 方时最好 - 其他服务器、文件系统等。由于您不想在单元测试中进行这些调用 - 模拟响应。

选项 2 - 我可以说最好的是,当您想要测试受保护/私有方法时,无需像您的代码那样“在途中”进行所有调用(使许多调用最终调用您要测试的方法),因为您只想测试特定的逻辑。当你的类有一些你想要测试的状态时,也很容易使用这个选项,而不需要编写很多代码来让你的类进入这个状态。

【讨论】:

  • 我将 jimmy_keen naswer 标记为解决方案,仅用于命名方法测试的示例。如果我可以标记两个答案也将标记您的(我只需添加 +1)
【解决方案3】:

您可以使用PrivateObject Class排列您的被测对象

允许测试代码调用被测代码的方法和属性 这将无法访问,因为它们不是公开的。

编辑

那么您可以通过PrivateObject.RealTypePrivateObject.Target 属性获得对包装对象的完全访问权限

编辑

无论如何,每个打破类隔离和封装的系统都会使 TDD 中的单元测试的黑盒方法变得毫无用处,应该作为毒药避免:)

【讨论】:

  • 这可能会导致您的应用程序以不同的流程运行。通过这种方式 child.parent 没有设置,可能会导致其他错误。 (在这种情况下可能不是,但在其他情况下可能)
  • @RvdK 你是完全正确的,无论如何 PrivateObject 可以在不需要复杂的反射模拟操作的情况下执行请求的操作
  • 我试一试:PrivateObject po = new PrivateObject(typeof(Tag)); po.SetProperty("父母", 父母);但是现在我怎样才能将此对象转换为需要作为参数调用 remove 方法的标记?
  • 谢谢,我同意您(和其他用户)关于封装中断的说明。我尝试学习新事物(我不得不说不喜欢魔术字符串的需要),但我会将 mzf 或 jimmy_keen 答案标记为解决方案。我给你加了 +1
  • @gt.guybrush 我同意你的决定 :)
【解决方案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 结果,表明是否实际执行了任何删除操作。见here

【讨论】:

  • 我使用了类似的 sintax,但使用了 Nunit 和 FluentAssertion(sintax 更具可读性:tag.Children.Should().NotContain(childTag))。布尔返回很有趣,但为什么连加法都不行?对于第一种情况,我不喜欢抛出异常,只是我什么都不做
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多