单元测试 - 我做得对吗?

时间:2010-05-05 09:19:18

标签: c# unit-testing collections tdd

基本上我已经编程了一段时间,在完成我的上一个项目之后可以完全理解如果我做了TDD会有多容易。我想我仍然没有严格执行,因为我仍然在编写代码,然后为它编写测试,如果你不知道什么结构以及你的存储数据等,我不太清楚测试在代码之前的变化......但无论如何......

有点难以解释,但基本上可以说例如我有一个具有id,颜色和成本等属性的Fruit对象。 (所有存储在textfile中的都完全忽略任何数据库逻辑等)

    FruitID FruitName   FruitColor  FruitCost
    1         Apple       Red         1.2
    2         Apple       Green       1.4
    3         Apple       HalfHalf    1.5

这只是一个例子。但是我要说这是这个结构中Fruit(它是一个List<Fruit>)对象的集合。如果水果被删除,我的逻辑会说要重新排序集合中的fruitids(这就是解决方案需要的方式)。

E.g。如果删除1,则对象2采用水果ID 1,对象3采用水果id2。

现在我想测试我写的代码,它会进行重新排序等等。

如何设置它以进行测试?


这是我到目前为止的地方。基本上我有所有方法的fruitManager类,如deletefruit等。它通常有列表,但我已经改变了hte方法来测试它,以便它接受一个列表,并删除水果上的信息,然后返回列表。

单位测试明智:我基本上是以正确的方式做到这一点,还是我的想法错了?然后我测试删除不同的有价值的对象/数据集,以确保方法正常工作。


[Test]
public void DeleteFruit()
{
    var fruitList = CreateFruitList();
    var fm = new FruitManager();

    var resultList = fm.DeleteFruitTest("Apple", 2, fruitList);

    //Assert that fruitobject with x properties is not in list ? how
}

private static List<Fruit> CreateFruitList()
{
    //Build test data
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...};
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...};
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...};

    var fruitList = new List<Fruit> {f01, f02, f03};
    return fruitList;
}

7 个答案:

答案 0 :(得分:12)

如果你没有看到应该从哪个测试开始,可能是你没有想到你的功能应该用简单的术语做什么。试着想象一下预期的基本行为的优先列表。

您对Delete()方法的第一件事是什么?如果您在10分钟内发送删除“产品”,那么包含哪些不可谈判的行为?嗯......可能是它删除了元素。

所以:

1) [Test]
public void Fruit_Is_Removed_From_List_When_Deleted()

当编写该测试时,请完成整个TDD循环(执行test =&gt; red;编写足够的代码使其通过=&gt; green; refactor =&gt; green)

与此相关的下一个重要事项是,如果作为参数传递的水果不在列表中,则该方法不应修改列表。所以接下来的测试可能是:

2) [Test]
public void Invalid_Fruit_Changes_Nothing_When_Deleted()

您指定的下一件事是,在删除水果时应重新排列ID:

3) [Test]
public void Fruit_Ids_Are_Reordered_When_Fruit_Is_Deleted()

该测试应该放什么?好吧,只需设置一个基本但有代表性的上下文,它将证明您的方法符合预期。

例如,创建一个包含4个水果的列表,删除第一个水果并逐个检查3个剩余水果ID是否正确重新排序。这很好地涵盖了基本情景。

然后你可以为错误或边缘情况创建单元测试:

4) [Test]
public void Fruit_Ids_Arent_Reordered_When_Last_Fruit_Is_Deleted()

5) [Test]
[ExpectedException]
public void Exception_Is_Thrown_When_Fruit_List_Is_Empty()

...

答案 1 :(得分:7)

在您真正开始编写第一个测试之前,您应该对应用程序的结构/设计,界面等有一个大致的了解。设计阶段通常与TDD有关。

我想对于一位经验丰富的开发人员而言,这很明显,并且在阅读问题规范后,他立即开始在他/她的头脑中看到解决方案的设计 - 这可能是它经常出现的原因。理所当然。但是,对于经验不足的开发人员来说,设计活动可能需要更明确的承诺。

无论哪种方式,在第一个设计草图准备就绪后, TDD既可用于验证行为,也可用于检查设计本身的可靠性/可用性。你可能会开始编写你的第一个单元测试,然后意识到“哦,用我想象的界面做这个实际上很尴尬” - 然后你回去重新设计界面。这是一种迭代方法。

Josh Bloch在“Coders at Work”中谈到了这一点 - 他通常在开始实现任何内容之前为他的接口写了很多用例。因此,他勾勒出界面,然后编写在他能想到的所有不同场景中使用它的代码。它还不具备可编辑性 - 他只是用它来感受他的界面是否真的有助于轻松完成任务。

答案 2 :(得分:3)

  

明智的单元测试:我基本上是以正确的方式做到这一点,还是我的想法错了?

你错过了这条船。

  

如果您不知道什么结构以及如何存储数据,我不太清楚测试在代码之前的变化

如果您希望这些想法有意义,我认为您需要回到这一点。

第一点:数据结构和存储源自您需要代码执行的操作,而不是相反。更详细地说,如果您从头开始,可以使用任意数量的结构/存储实现;实际上,您应该能够在它们之间进行交换而无需更改测试。

第二点:在大多数情况下,您比使用代码更频繁地使用代码。你写了一次,但你(和你的同事)多次打电话。因此,调用代码的便利性应该比你纯粹从内到外编写解决方案时获得更高的优先级。

因此,当您发现自己编写测试并发现客户端实现丑陋/笨拙/不合适时,它会在您开始实现任何操作之前向您发出警告。同样,如果您发现自己在测试中编写了大量的设置代码,它会告诉您,您并没有真正将您的问题分开。当你发现自己说“哇,那个测试很容易写”时,你可能会有一个易于使用的界面。

当您使用面向实现的示例(比如为容器编写测试)时,很难实现这一点。你需要的是一个有限的玩具问题,与实施无关。

对于一个简单的例子,您可以考虑一个身份验证管理器 - 传入一个标识符和一个秘密,并找出该秘密是否与该标识符匹配。因此,您应该能够在顶部编写三个快速测试:验证正确的密码是否允许访问,验证不正确的密码是否禁止访问,验证更改密码时,只有新版本允许访问。

因此,您可能会使用用户名和密码编写一些简单的测试。当你这样做时,你意识到秘密不应该局限于字符串,而是你应该能够从可序列化的任何东西中产生秘密,并且可能访问不是通用的,而是受限制的(这与身份验证管理器有关) ?也许不是)哦,你要证明秘密是安全的......

当然,您可以对容器采用相同的方法。但是,如果您从用户/业务问题开始而不是实现问题,我认为您会发现“获取它”更容易。

验证特定实现的单元测试(“我们这里有栅栏发布错误吗?”)具有价值。创建它们的过程更像是“猜错,编写测试以检查错误,如果测试失败则做出反应”。但是,这些测试往往不会影响您的设计 - 您更有可能克隆代码块并更改某些输入。但是,通常情况下,当单元测试遵循实现时,它们通常很难编写并且具有大的启动成本(“为什么我需要加载三个库并启动远程Web服务器来测试for循环中的fencepost错误?“)。

推荐阅读 Freeman / Pryce,不断增长的面向对象软件,以测试为导向

答案 3 :(得分:1)

您永远不会确定您的单元测试涵盖了所有可能的情况,因此您或多或少都会考虑您的测试范围以及测试内容。你的单元测试应该至少测试你没有在那里做的边界情况。当您尝试删除ID无效的Apple时会发生什么?如果您有一个空列表会发生什么,如果删除第一个/最后一个项目会怎么样

一般来说,我没有像上面那样测试一个特殊情况。相反,我总是尝试运行一系列测试,在您的示例中建议采用略有不同的方法:

  • 首先,编写一个检查方法。一旦你知道你将拥有一个水果列表,你就可以做到这一点。在这个列表中,所有水果都会有连续的ID(就像测试列表是否排序一样)。不必为此编写删除代码,此外您可以在以后重新使用它f.ex.在单元测试插入代码中。

  • 然后,创建一堆不同的(可能是随机的)测试列表(空白大小,平均大小,大尺寸)。这也不需要事先删除代码。

  • 最后,为每个测试列表运行特定删除(使用无效ID删除,删除id 1,删除最后一个id,删除随机ID)并使用checker方法检查结果。此时,您至少应该知道删除方法的界面,但不需要已经编写过。

关于评论的@Update: 检查器方法更多的是对数据结构的一致性检查。在您的示例中,列表中的所有水果都有连续的ID,因此会进行检查。如果你有DAG结构,你可能想检查它的分类等。

测试ID x的删除是否有效取决于它是否完全存在于列表中,以及您的应用程序是否区分由于无效ID而导致的删除失败的情况(因为没有这样的ID)留在最后)。显然,您还需要验证列表中是否已不再存在已删除的ID(尽管这不是我使用检查器方法的意思的一部分 - 相反,我认为很明显可以省略)。

答案 4 :(得分:1)

由于您正在使用C#,我将假设NUnit是您的测试框架。在这种情况下,您可以使用一系列Assert [..]语句。

关于代码的细节:在操作列表时,我不会以任何方式重新分配ID或更改剩余Fruit对象的构成。如果您需要id来跟踪列表中对象的位置,请改用.IndexOf()。

使用TDD,我发现首先编写测试通常很难做到 - 我最终编写代码(代码或一串黑客)。然后一个好方法是采用“代码”,并将其用作测试。然后再次编写实际代码 ,略有不同。通过这种方式,您将拥有两个不同的代码片段来完成同样的事情 - 在生产和测试代码中犯同样错误的可能性更小。此外,必须针对同一问题提出第二种解决方案,可能会显示原始方法的弱点,并导致更好的代码。

答案 5 :(得分:1)

[Test]
public void DeleteFruit()
{
    var fruitList = CreateFruitList();
    var fm = new FruitManager(fruitList);

    var resultList = fm.DeleteFruit(2);

    //Assert that fruitobject with x properties is not in list
    Assert.IsEqual(fruitList[2], fm.Find(2));
}

private static List<Fruit> CreateFruitList()
{
    //Build test data
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...};
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...};
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...};

    return new List<Fruit> {f01, f02, f03};
}

您可以尝试对水果列表进行依赖注入。水果经理对象是一个crud商店。因此,如果您有删除操作,则需要检索操作。

关于重新排序,您希望它自动发生还是您想要进行度假操作。自动也可以在删除操作发生时或者仅在检索时延迟。这是一个实现细节。关于这一点还有很多可以说的。掌握这个具体例子的一个良好开端是使用Design By Contract。

[编辑1a]

另外,您可能想要考虑为什么要对 Fruit 的特定实现进行测试。 FruitManager应该管理一个名为Fruit的抽象概念。您需要注意过早的实现细节,除非您希望使用DTO的路线,但问题是Fruit最终可能会从具有getter的对象更改为具有实际行为的对象。现在,Fruit的测试不仅会失败,而且FruitManager也会失败!

答案 6 :(得分:1)

从界面开始,有一个骨架具体实现。对于每个方法/属性/事件/构造函数,都存在预期的行为。从第一个行为的规范开始,并完成它:

[规格]与[TestFixture]相同 [它]与[测试]

相同
[Specification]
When_fruit_manager_has_delete_called_with_existing_fruit : FruitManagerSpecifcation
{
  private IEnumerable<IFruit> _fruits;

  [It]
  public void Should_remove_the_expected_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  [It]
  public void Should_not_remove_any_other_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  [It]
  public void Should_reorder_the_ids_of_the_remaining_fruit()
  {
    Assert.Inconclusive("Please implement");
  }

  /// <summary>
  /// Setup the SUT before creation
  /// </summary>
  public override void GivenThat()
  {
     _fruits = new List<IFruit>();

     3.Times(_fruits.Add(Mock<IFruit>()));

     this._fruitToDelete = _fruits[1];

     // this fruit is injected in th Sut
     Dep<IEnumerable<IFruit>>()
                .Stub(f => ((IEnumerable)f).GetEnumerator())
                .Return(this.Fruits.GetEnumerator())
                .WhenCalled(mi => mi.ReturnValue = this._fruits.GetEnumerator());

  }

  /// <summary>
  /// Delete a fruit
  /// </summary>
  public override void WhenIRun()
  {
    Sut.Delete(this._fruitToDelete);
  }
}

上面的规范只是adhoc和INCOMPLETE,但这是一种很好的行为TDD方式来接近每个单元/规范。

当你第一次开始研究它时,这将是未实现的SUT的一部分:

public interface IFruitManager
{
  IEnumerable<IFruit> Fruits { get; }

  void Delete(IFruit);
}

public class FruitManager : IFruitManager
{
   public FruitManager(IEnumerable<IFruit> fruits)
   {
     //not implemented
   }

   public IEnumerable<IFruit> Fruits { get; private set; }

   public void Delete(IFruit fruit)
   {
    // not implemented
   }
}

因此,您可以看到没有编写真正的代码。如果你想完成第一个“When _...”规范,你实际上首先必须做一个[ConstructorSpecification] When_fruit_manager_is_injected_with_fruit(),因为注入的果实没有被分配给Fruits属性。

瞧,首先不需要真正的代码......现在唯一需要的就是纪律。

我喜欢这一点,如果你在实现当前SUT期间需要额外的类,你不必在实现FruitManager之前实现它们,因为你可以使用类似例如ISomeDependencyNeeded的模拟......以及何时你完成了Fruit manager,然后你可以继续使用SomeDependencyNeeded类。非常邪恶。