【问题标题】:Are those unit tests fine?这些单元测试好吗?
【发布时间】:2009-04-07 18:56:44
【问题描述】:

我正在尝试掌握测试驱动开发,我想知道这些单元测试是否合适。我有一个如下所示的界面:

public interface IEntryRepository
{
    IEnumerable<Entry> FetchAll();
    Entry Fetch(int id);
    void Add(Entry entry);
    void Delete(Entry entry);
}

然后是实现该接口的这个类:

public class EntryRepository : IEntryRepository
{
    public List<Entry> Entries {get; set; }

    public EntryRepository()
    {
        Entries = new List<Entry>();
    }

    public IEnumerable<Entry> FetchAll()
    {
        throw new NotImplementedException();
    }

    public Entry Fetch(int id)
    {
        return Entries.SingleOrDefault(e => e.ID == id);
    }

    public void Add(Entry entry)
    {
        Entries.Add(entry);
    }

    public void Delete(Entry entry)
    {
        Entries.Remove(entry);
    }
}

这些是我到目前为止编写的单元测试,它们还不错还是我应该做一些不同的事情?我应该嘲笑 EntryRepository 吗?

[TestClass]
public class EntryRepositoryTests
{
    private EntryRepository rep;

    public EntryRepositoryTests()
    {
        rep = new EntryRepository();
    }

    [TestMethod]
    public void TestAddEntry()
    {
        Entry e = new Entry { ID = 1, Date = DateTime.Now, Task = "Testing" };
        rep.Add(e);

        Assert.AreEqual(1, rep.Entries.Count, "Add entry failed");
    }

    [TestMethod]
    public void TestRemoveEntry()
    {
        Entry e = new Entry { ID = 1, Date = DateTime.Now, Task = "Testing" };
        rep.Add(e);

        rep.Delete(e);
        Assert.AreEqual(null, rep.Entries.SingleOrDefault(i => i.ID == 1), "Delete entry failed");
    }

    [TestMethod]
    public void TestFetchEntry()
    {
        Entry e = new Entry { ID = 2, Date = DateTime.Now, Task = "Testing" };
        rep.Add(e);

        Assert.AreEqual(2, rep.Fetch(2).ID, "Fetch entry failed");
    }
}

谢谢!

【问题讨论】:

  • 出于好奇,您使用的是什么测试框架?我也在尝试启动 TDD 并正在寻找建议。
  • 我在 Visual Studio 2008 中使用内置的 tdd 功能
  • 您可能想为您的问题想一个新标题,“这些单元测试好吗?”相当通用。
  • 啊。哦,好吧 :-)(我使用的是 Express Edition)
  • @person-b:NUnit 是一个流行/免费的 C# 测试框架。

标签: c# .net unit-testing tdd mocking


【解决方案1】:

就在我的头顶......

虽然您对 add 的测试实际上只测试了框架:

  • 您已添加 1 项,这很好
  • 如何添加很多项目 (我的意思是,荒谬的数量 - 容器添加失败的 n 个条目的值是多少?)
  • 不添加项目怎么样? (空条目)
  • 如果您将项目添加到列表中,它们是否按特定顺序排列? 应该是吗?

您的 fetch 也是如此:

  • 如果 x > rep.Count ,您的 fetch(x) 会发生什么?
  • 如果 x
  • 如果代表为空怎么办?
  • x 是否符合性能要求(它的算法是什么? 复杂?只有一个条目时是否在范围内? 参赛作品数量多得离谱?

Pragmatic Unit Testing这本书里有一个很好的清单(好书,强烈推荐)

  • 结果对吗?
  • 所有边界条件是否正确
    • 符合预期格式
    • 订购正确
    • 在合理范围内
    • 它是否引用任何外部依赖项
    • 基数是否正确? (正确的值数)
    • 它是否在正确的时间量(真实或相对)内完成
  • 你能检查反向关系吗
  • 您能否用另一种经过验证的方法交叉检查结果
  • 你能强制错误条件吗
  • 性能特征是否在界限内

【讨论】:

    【解决方案2】:

    以下是一些想法:

    正面

    • 你是单元测试!
    • 您遵循约定 Arrange, Act, Assert

    否定

    • 没有条目时删除条目的测试在哪里?
    • 没有条目时获取条目的测试在哪里?
    • 当您添加两个条目并删除一个条目时会发生什么?应该留下哪一个?
    • 应该Entries 公开。您的一个断言调用 rep.Entries.SingleOrDefault 这一事实向我表明您没有正确构造该类。
    • 您的测试命名有点模糊;通常一个好的模式是:{MethodName}_{Context}_{Expected Behavior} 删除冗余“测试”冗余。

    作为 TDD 的初学者,我发现 Test-Driven Development By Example 这本书对我有很大帮助。其次,Roy Osherove 有一些很好的 Test Review 视频教程,看看吧。

    【讨论】:

      【解决方案3】:

      在我回答之前,让我声明我对单元测试相当陌生,绝不是专家,所以我对我所说的一切持保留态度。

      但我觉得您的单元测试在很大程度上是多余的。您的许多方法都是简单的传递,例如您的 AddEntry 方法只是对基础 List 方法 Add 的调用。您不是在测试您的代码,而是在测试 Java 库。

      我只推荐包含您编写的逻辑的单元测试方法。避免测试像 getter 和 setter 这样明显的方法,因为它们在最基本的级别上运行。这是我的理念,但我知道有些人确实相信测试显而易见的方法,我只是碰巧认为这是毫无意义的。

      【讨论】:

      • 老兄记住它是 TDD。目前还没有实施!
      • 当有人更改“添加”方法而您没有涵盖现有行为的测试时会发生什么?甚至在开发主要代码之前就编写了测试,它们描述了系统应该/将如何运行。您的建议违反了 TDD 原则。
      • 啊,也许问题是问题还为时过早,我以为他只是在编写多余的单元测试。
      • 从纯 TDD 的角度来看,你们是完全正确的。我听说有人在实践中改变了 TDD 的规则,以避免像我上面概述的那样不必要的单元测试。
      【解决方案4】:

      这样看起来不错。我个人喜欢给我的测试起一个更具描述性的名称,但这更多的是个人喜好。

      您可以对您正在测试的类的依赖项使用模拟,EntryRepository 是被测类,因此无需模拟它,否则您最终将测试模拟实现而不是类。

      只是举一个简单的例子。如果您的 EntryRepository 将使用后端数据库来存储条目而不是列表,那么您可以为数据访问内容注入模拟实现,而不是调用真实的数据库。

      【讨论】:

        【解决方案5】:

        这看起来是一个不错的开始,但您应该尽可能多地尝试测试“边界”案例。考虑一下可能导致您的方法失败的原因 - 将 null 条目传递给 Add 或 Delete 是否有效?尝试编写测试所有可能的代码路径。如果您对代码进行任何更改,以这种方式编写测试将使您的测试套件在未来更加有用。

        此外,每个测试方法都可以使测试对象保持与调用时相同的状态。我注意到您的 TestFetchEntry 方法向 EntryRepository 添加了一个元素,但从未将其删除。让每个方法都不会影响测试对象的状态可以更轻松地运行一系列测试。

        【讨论】:

          【解决方案6】:

          您不应该嘲笑 IEntryRepository,因为实现类是被测试的类。您可能想要模拟List&lt;Entry&gt; 并注入它,然后只需测试您通过公共接口调用的方法是否被正确调用。这只是您实现它的方式的替代方案,并且不一定更好 - 除非您希望该类注入它的支持类,在这种情况下以这种方式编写测试会强制执行该行为。

          您可能需要更多测试以确保在插入条目时插入正确的条目。与删除类似——插入几个条目,然后删除一个并确保正确的一个已被删除。一旦你想出测试来让代码做你想做的事,继续思考你可能会在编写代码时搞砸的方法,并编写测试以确保这些不会发生。当然,这门课非常简单,您也许可以说服自己,您对驱动器行为所做的测试就足够了。不过,它并不需要太多复杂性,就值得测试边缘情况和意外行为。

          【讨论】:

            【解决方案7】:

            对于 TDD 初学者和这个特定的课程,您的测试很好。 +1 表示努力。

            一旦遇到涉及依赖注入和模拟的更复杂场景,请发布另一个问题。这就是事情变得非常有趣的地方;)。

            【讨论】:

              【解决方案8】:

              整体看起来不错。您应该使用事务(或在 TestInitialize 中创建存储库的新实例)来确保测试是真正隔离的。

              还可以使用更具描述性的测试方法,例如When_a_new_entity_is_added_to_a_EntryRepository_the_total_count_of_objects_should_get_incremented

              【讨论】:

              • 你的意思是我应该在[TestInitialize]中初始化我的EntryRepository,并在[TestCleanup]中销毁它,这样每个测试都有一个干净的空存储库吗?
              • 这种命名方式让我抓狂。我非常注重命名方法的细节,但简洁是必须的。测试代码中仍然存在注释。
              • @Alexander Nyquist - 是的,我就是这个意思。
              • @SnOrfus - 同意,这个名字无法读取,也不会被读取。
              • 请注意我的测试名称如何强调测试验证的确切内容(而不是验证添加功能,这可能与计数器分开实现!!!)
              猜你喜欢
              • 2017-11-27
              • 2019-11-03
              • 2021-12-20
              • 2018-03-13
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-10-02
              相关资源
              最近更新 更多