使用 C#,我需要一个名为 User 具有用户名、密码、活动标志、名字、姓氏、全名等。

应该有方法 认证节省 一个用户。我只为方法编写测试吗?我是否还需要担心测试这些属性,因为它们是 .Net 的 getter 和 setter?

有帮助吗?

解决方案

对于我的问题,也有很多对此的精彩回应:”开始 TDD - 挑战?解决方案?建议?"

我还可以建议看看我的 博客文章 (部分是受到我的问题的启发),我对此得到了一些很好的反馈。即:

我不知道从哪里开始?

  • 重新开始。仅在编写新代码时考虑编写测试。这可以重新处理旧代码,也可以是全新的功能。
  • 从简单开始。不要逃跑,试图使自己的头绕过测试框架以及TDD风格。Debug.Assert 工作正常。使用它作为起点。它不会弄乱您的项目或创建依赖关系。
  • 开始积极。您正在尝试改善自己的工艺,对此感到满意。我已经看到很多开发人员乐于停滞不前,而不是尝试新事物来改善自己。您正在做正确的事,请记住这一点,这将有助于阻止您放弃。
  • 开始准备迎接挑战。很难开始进行测试。期待挑战,但请记住 - 可以克服挑战。

只测试您期望的内容

我刚开始时遇到了真正的问题,因为我一直坐在那里试图找出可能发生的所有可能问题,然后试图测试它并修复。这是解决头痛的快速方法。测试应该是一个真正的Yagni过程。如果您知道有问题,请为其编写测试。否则,别打扰。

只测试一件事

每个测试用例只能测试一件事。如果您发现自己将“和”放在测试用例名称中,那么您做错了什么。

我希望这意味着我们可以从“getters 和 setters”继续前进:)

其他提示

测试您的代码,而不是语言。

单元测试如下:

Integer i = new Integer(7);
assert (i.instanceOf(integer));

仅当您正在编写编译器并且您的编译器有非零的机会时才有用 instanceof 方法不起作用。

不要测试可以依赖语言来执行的东西。就您而言,我将重点关注您的身份验证和保存方法 - 并且我将编写测试以确保它们可以优雅地处理任何或所有这些字段中的空值。

这让我进入了单元测试,这让我非常高兴

我们刚刚开始做单元测试。很长一段时间以来,我都知道开始这样做会很好,但我不知道如何开始,更重要的是要测试什么。

然后我们必须重写会计程序中的一段重要代码。这部分非常复杂,因为它涉及很多不同的场景。我所说的部分是一种支付已输入会计系统的销售和/或采购发票的方法。

我只是不知道如何开始编码,因为有很多不同的付款选项。发票可能是 100 美元,但客户只转了 99 美元。也许您已将销售发票发送给客户,但您也从该客户处购买了产品。所以你以 300 美元的价格把他卖了,但你却以 100 美元的价格买了他。您可以预期您的客户将向您支付 200 美元来结清余额。如果您的售价为 500 美元,但客户只付给您 250 美元怎么办?

因此,我有一个非常复杂的问题需要解决,其中有很多可能性,一种情况可以完美地工作,但在其他类型的发票/付款组合上可能会出错。

这就是单元测试发挥作用的地方。

我开始(在测试代码中)编写一种方法来创建销售和采购发票列表。然后我编写了第二种方法来创建实际付款。通常,用户会通过用户界面输入该信息。

然后我创建了第一个测试方法,测试单个发票的非常简单的付款,没有任何付款折扣。当银行付款保存到数据库时,系统中的所有操作都会发生。正如您所看到的,我创建了一张发票,创建了一笔付款(银行交易)并将交易保存到磁盘。在我的断言中,我输入了银行交易和链接发票中最终出现的正确数字。交易后我检查付款次数、付款金额、折扣金额和发票余额。

测试运行后,我会访问数据库并仔细检查是否存在我期望的内容。

我编写了测试,开始编写付款方法(BankHeader 类的一部分)。在编码中,我只关心代码以使第一个测试通过。我还没有考虑其他更复杂的场景。

我运行了第一个测试,修复了一个小错误,直到测试通过。

然后我开始编写第二个测试,这次使用付款折扣。编写测试后,我修改了付款方式以支持折扣。

在测试付款折扣的正确性时,我还测试了简单付款。当然,这两项测试都应该通过。

然后我开始研究更复杂的场景。

1)想出一个新的场景

2)为该场景编写测试

3)运行单个测试看看它是否会通过

4)如果没有,我会调试并修改代码,直到它通过。

5)修改代码时我继续运行所有测试

这就是我如何创建非常复杂的付款方式的方法。如果没有单元测试,我不知道如何开始编码,这个问题似乎势不可挡。通过测试,我可以从一个简单的方法开始,然后逐步扩展它,以确保更简单的场景仍然有效。

我确信使用单元测试节省了我几天(或几周)的编码时间,并且或多或少保证了我的方法的正确性。

如果我以后想到一个新场景,我可以将其添加到测试中,看看它是否有效。如果不是,我可以修改代码,但仍然确保其他场景仍然正常工作。这将节省维护和错误修复阶段的时间。

是的,如果用户做了你没有想到或阻止他做的事情,即使经过测试的代码仍然可能存在错误

以下是我为测试付款方式而创建的一些测试。

public class TestPayments
{
    InvoiceDiaryHeader invoiceHeader = null;
    InvoiceDiaryDetail invoiceDetail = null;
    BankCashDiaryHeader bankHeader = null;
    BankCashDiaryDetail bankDetail = null;



    public InvoiceDiaryHeader CreateSales(string amountIncVat, bool sales, int invoiceNumber, string date)
    {
        ......
        ......
    }

    public BankCashDiaryHeader CreateMultiplePayments(IList<InvoiceDiaryHeader> invoices, int headerNumber, decimal amount, decimal discount)
    {
       ......
       ......
       ......
    }


    [TestMethod]
    public void TestSingleSalesPaymentNoDiscount()
    {
        IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
        list.Add(CreateSales("119", true, 1, "01-09-2008"));
        bankHeader = CreateMultiplePayments(list, 1, 119.00M, 0);
        bankHeader.Save();

        Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
        Assert.AreEqual(1, bankHeader.BankCashDetails[0].Payments.Count);
        Assert.AreEqual(119M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
        Assert.AreEqual(0M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
    }

    [TestMethod]
    public void TestSingleSalesPaymentDiscount()
    {
        IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
        list.Add(CreateSales("119", true, 2, "01-09-2008"));
        bankHeader = CreateMultiplePayments(list, 2, 118.00M, 1M);
        bankHeader.Save();

        Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
        Assert.AreEqual(1, bankHeader.BankCashDetails[0].Payments.Count);
        Assert.AreEqual(118M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
        Assert.AreEqual(1M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
    }

    [TestMethod]
    [ExpectedException(typeof(ApplicationException))]
    public void TestDuplicateInvoiceNumber()
    {
        IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
        list.Add(CreateSales("100", true, 2, "01-09-2008"));
        list.Add(CreateSales("200", true, 2, "01-09-2008"));

        bankHeader = CreateMultiplePayments(list, 3, 300, 0);
        bankHeader.Save();
        Assert.Fail("expected an ApplicationException");
    }

    [TestMethod]
    public void TestMultipleSalesPaymentWithPaymentDiscount()
    {
        IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
        list.Add(CreateSales("119", true, 11, "01-09-2008"));
        list.Add(CreateSales("400", true, 12, "02-09-2008"));
        list.Add(CreateSales("600", true, 13, "03-09-2008"));
        list.Add(CreateSales("25,40", true, 14, "04-09-2008"));

        bankHeader = CreateMultiplePayments(list, 5, 1144.00M, 0.40M);
        bankHeader.Save();

        Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
        Assert.AreEqual(4, bankHeader.BankCashDetails[0].Payments.Count);
        Assert.AreEqual(118.60M, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
        Assert.AreEqual(400, bankHeader.BankCashDetails[0].Payments[1].PaymentAmount);
        Assert.AreEqual(600, bankHeader.BankCashDetails[0].Payments[2].PaymentAmount);
        Assert.AreEqual(25.40M, bankHeader.BankCashDetails[0].Payments[3].PaymentAmount);

        Assert.AreEqual(0.40M, bankHeader.BankCashDetails[0].Payments[0].PaymentDiscount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].PaymentDiscount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[2].PaymentDiscount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].PaymentDiscount);

        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].InvoiceHeader.Balance);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[2].InvoiceHeader.Balance);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].InvoiceHeader.Balance);
    }

    [TestMethod]
    public void TestSettlement()
    {
        IList<InvoiceDiaryHeader> list = new List<InvoiceDiaryHeader>();
        list.Add(CreateSales("300", true, 43, "01-09-2008")); //Sales
        list.Add(CreateSales("100", false, 6453, "02-09-2008")); //Purchase

        bankHeader = CreateMultiplePayments(list, 22, 200, 0);
        bankHeader.Save();

        Assert.AreEqual(1, bankHeader.BankCashDetails.Count);
        Assert.AreEqual(2, bankHeader.BankCashDetails[0].Payments.Count);
        Assert.AreEqual(300, bankHeader.BankCashDetails[0].Payments[0].PaymentAmount);
        Assert.AreEqual(-100, bankHeader.BankCashDetails[0].Payments[1].PaymentAmount);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[0].InvoiceHeader.Balance);
        Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[1].InvoiceHeader.Balance);
    }

如果它们真的很微不足道,那么就不要费心去测试。例如,如果它们是这样实现的;

public class User
{
    public string Username { get; set; }
    public string Password { get; set; }
}

另一方面,如果您正在做一些聪明的事情(例如在 getter/setter 中加密和解密密码),那么请对其进行测试。

规则是你必须测试你编写的每一段逻辑。如果您在 getter 和 setter 中实现了某些特定功能,我认为它们值得测试。如果他们只为某些私有字段赋值,请不要打扰。

这个问题似乎是一个关于哪些方法被测试、哪些不被测试的界限在哪里的问题。

用于值分配的 setter 和 getter 在创建时考虑了一致性和未来的增长,并预见到一段时间内 setter/getter 可能会演变成更复杂的操作。对这些方法进行单元测试是有意义的,也是为了一致性和未来的增长。

代码可靠性,尤其是在进行更改以添加附加功能时,是主要目标。我不知道有谁因为在测试方法中包含 setter/getter 而被解雇,但我确信有人希望他们测试过的方法最后他们知道或可以回忆起来是简单的 set/get 包装器,但事实并非如此情况较长。

也许团队的另一名成员扩展了 set/get 方法以包含现在需要测试但随后没有创建测试的逻辑。但现在你的代码正在调用这些方法,你不知道它们发生了变化,需要深入的测试,并且你在开发和 QA 中所做的测试不会触发缺陷,但发布第一天的真实业务数据会触发缺陷触发它。

现在,当集合/变得包含可能失败但单元测试未涵盖的逻辑时,两个队友将争论谁丢球并未能进行单元测试。如果从第一天开始就在简单的 set/gets 上实施测试,那么最初编写 set/gets 的队友将会更容易地完成这个清理工作。

我的观点是,用单元测试覆盖所有方法(即使是微不足道的方法)的几分钟“浪费”时间可能会避免日后数天的头痛、金钱/业务声誉的损失以及某人的工作的损失。

事实上,你确实用单元测试包装了琐碎的方法,当初级队友将琐碎的方法更改为非琐碎的方法并提示他们更新测试时,他们可能会看到,现在没有人遇到麻烦,因为缺陷已得到控制从达到生产。

我们的编码方式,以及从我们的代码中可以看出的纪律,可以帮助其他人。

另一个规范的答案。我相信,这是罗恩·杰弗里斯(Ron Jeffries)说的:

仅测试您想要运行的代码。

测试样板代码是浪费时间,但正如 Slavo 所说,如果您向 getter/setter 添加副作用,那么您应该编写一个测试来配合该功能。

如果您正在进行测试驱动开发,则应该首先编写契约(例如接口),然后编写测试来执行该接口,该接口记录了预期的结果/行为。 然后 自己编写方法,而无需触及单元测试中的代码。最后,使用代码覆盖工具并确保您的测试执行代码中的所有逻辑路径。

像 getter 和 setter 这样的非常简单的代码除了设置私有字段之外没有额外的行为,对于测试来说是多余的。在 3.0 中,C# 甚至有一些语法糖,编译器会处理私有字段,因此您不必对其进行编程。

我通常会编写许多非常简单的测试来验证我期望从类中获得的行为。即使是简单的事情,例如两个数字相加。我在编写简单测试和编写几行代码之间切换很多。这样做的原因是,我可以更改代码,而不必担心我破坏了我没有想到的东西。

你应该测试一切。现在你有 getter 和 setter,但有一天你可能会对它们进行一些更改,也许是为了进行验证或其他操作。您今天编写的测试将在明天使用,以确保一切正常运行。当你编写测试时,你应该忘记诸如“现在这很微不足道”之类的考虑。在敏捷或测试驱动的环境中,您应该假设未来重构进行测试。另外,您是否尝试过输入非常奇怪的值,例如极长的字符串或其他“坏”内容?嗯,你应该...永远不要假设你的代码将来会被滥用到多么严重的程度。

一般来说,我发现编写大量的用户测试一方面令人筋疲力尽。另一方面,尽管它总是为您提供有关应用程序应如何工作的宝贵见解,并帮助您抛弃简单(且错误)的假设(例如:用户名的长度始终小于 1000 个字符)。

对于最终可能出现在工具包或开源类型项目中的简单模块,您应该尽可能多地进行测试,包括简单的 getter 和 setter。您要记住的是,在编写特定模块时生成单元测试相当简单且直接。添加 getter 和 setter 是最少的代码,无需太多思考即可处理。然而,一旦您的代码被放置在更大的系统中,这种额外的努力可以保护您免受底层系统更改的影响,例如基类中的类型更改。测试一切是完成回归的最佳方法。

为 getter 和 setter 编写单元测试并没有什么坏处。现在,他们可能只是在幕后进行字段获取/设置,但将来您可能会有验证逻辑或需要测试的属性间依赖关系。现在一边思考一边写出来会更容易,然后在那个时候记得修改它。

一般来说,当只为某些值定义方法时,请测试以下值: 以及以上 可接受的边界。换句话说,确保你的方法做了它应该做的事情, 但仅此而已. 。这很重要,因为当你要失败时,你想尽早失败。

在继承层次结构中,请确保测试 LSP 遵守。

测试默认的 getter 和 setter 对我来说似乎不太有用,除非您计划稍后进行一些验证。

据我了解敏捷开发背景下的单元测试,Mike,是的,您需要测试 getter 和 setter(假设它们是公开可见的)。单元测试的整个概念是测试软件单元,在本例中是一个类,作为一个 黑盒子. 。由于 getter 和 setter 是外部可见的,因此您需要将它们与“身份验证”和“保存”一起测试。

如果 Authenticate 和 Save 方法使用这些属性,那么您的测试将间接触及这些属性。只要属性只是提供对数据的访问,那么就不需要显式测试(除非您想要 100% 的覆盖率)。

我会测试你的 getter 和 setter。根据编写代码的人的不同,有些人会更改 getter/setter 方法的含义。我已经看到变量初始化和其他验证是 getter 方法的一部分。为了测试此类事情,您需要明确覆盖该代码的单元测试。

就我个人而言,我会“测试任何可能破坏的东西”,简单的吸气剂(甚至更好的自动属性)不会破坏。我从来没有遇到过简单的 return 语句失败的情况,因此也从未对它们进行过测试。如果吸气剂内部有计算或其他形式的语句,我肯定会为它们添加测试。

我个人使用 起订量 作为模拟对象框架,然后验证我的对象是否按其应有的方式调用周围的对象。

你必须用UT覆盖类的每个方法的执行并检查方法的返回值。这包括 getter 和 setter,特别是在成员(属性)是复杂类的情况下,在初始化期间需要大量内存分配。例如,使用一些非常大的字符串(或带有希腊符号的字符串)调用设置器,并检查结果是否正确(未截断,编码良好等)

如果简单整数也适用 - 如果传递 long 而不是整数会发生什么?这就是你写UT的原因:)

我不会测试属性的实际设置。我更关心消费者如何填充这些属性,以及他们填充什么。对于任何测试,您都必须权衡风险与测试时间/成本。

您应该尽可能使用单元测试来测试“每个重要的代码块”。

如果您的属性很琐碎并且不太可能有人会在其中引入错误,那么不对其进行单元测试应该是安全的。

您的 Authenticate() 和 Save() 方法看起来很适合测试。

理想情况下,您应该在编写课程时完成单元测试。这就是使用测试驱动开发时应该做的事情。您在实现每个功能点时添加测试,确保您也用测试覆盖边缘情况。

之后编写测试要痛苦得多,但也是可行的。

如果我处于你的位置,我会这样做:

  1. 编写一组基本测试来测试核心功能。
  2. 获取 NOver 并在您的测试中运行它。此时您的测试覆盖率可能约为 50%。
  3. 不断添加覆盖边缘情况的测试,直到覆盖率达到 80%-90% 左右

这应该为您提供一组很好的单元测试工作集,它们将充当针对回归的良好缓冲。

这种方法的唯一问题是代码必须是 设计的 以这种方式进行测试。如果您早期犯了任何耦合错误,您将无法轻松获得高覆盖率。

这就是为什么在编写代码之前编写测试非常重要。它迫使您编写松散耦合的代码。

不要测试明显有效的(样板)代码。因此,如果您的 setter 和 getter 只是“propertyvalue = value”和“return propertyvalue”,那么测试它是没有意义的。

即使 get / set 也可能产生奇怪的后果,具体取决于它们的实现方式,因此它们应该被视为方法。

这些测试的每个测试都需要指定属性的参数集,定义可接受和不可接受的属性,以确保调用以预期方式返回/失败。

您还需要了解安全陷阱(例如 SQL 注入)并进行测试。

所以是的,您确实需要担心测试属性。

我认为当 getter 和 setter 只进行简单操作时测试它们是愚蠢的。就我个人而言,我不会编写复杂的单元测试来涵盖任何使用模式。我尝试编写足够的测试来确保我已经处理了正常的执行行为以及我能想到的尽可能多的错误情况。我将编写更多单元测试作为对错误报告的回应。我使用单元测试来确保代码满足要求并使将来的修改更容易。当我知道如果我破坏某些东西测试就会失败时,我会更愿意更改代码。

我会为您正在编写的任何可在 GUI 界面之外进行测试的代码编写一个测试。

通常,我编写的任何具有放置在另一层或业务逻辑层内的业务逻辑的逻辑。

那么为任何做某事的事情编写测试就很容易做到。

首先,为“业务逻辑层”中的每个公共方法编写一个单元测试。

如果我有一堂这样的课:

   public class AccountService
    {
        public void DebitAccount(int accountNumber, double amount)
        {

        }

        public void CreditAccount(int accountNumber, double amount)
        {

        }

        public void CloseAccount(int accountNumber)
        {

        }
    }

在编写任何代码之前,我知道要执行这些操作,要做的第一件事就是开始编写单元测试。

   [TestFixture]
    public class AccountServiceTests
    {
        [Test]
        public void DebitAccountTest()
        {

        }

        [Test]
        public void CreditAccountTest()
        {

        }

        [Test]
        public void CloseAccountTest()
        {

        }
    }

编写测试来验证您编写的执行某些操作的代码。如果您迭代一组事物,并更改每个事物的某些内容,请编写一个执行相同操作的测试,并断言实际发生的情况。

您还可以采用许多其他方法,即行为驱动开发(BDD),它涉及更多,但不是开始单元测试技能的好地方。

所以,这个故事的寓意是,测试任何你可能担心的事情,让单元测试测试规模较小的特定事物,很多测试都是好的。

将您的业务逻辑保留在用户界面层之外,以便您可以轻松地为它们编写测试,并且您会做得很好。

我建议 测试驱动.Net 或者 锐锐 因为两者都可以轻松集成到 Visual Studio 中。

好吧,如果您认为它可能会损坏,请为其编写一个测试。我通常不测试 setter/getter,但是假设您为 User.Name 创建一个,它将名字和姓氏连接起来,我会编写一个测试,这样如果有人更改姓氏和名字的顺序,至少他会知道他改变了一些经过测试的东西。

规范的答案是“测试任何可能破裂的东西”。如果您确定属性不会破裂,请不要测试它们。

一旦发现某些东西坏了(你发现了一个错误),显然这意味着你需要测试它。编写一个测试来重现错误,观察它失败,然后修复错误,然后观察测试通过。

我建议为您的身份验证和保存方法编写多个测试。除了成功案例(提供所有参数、所有内容拼写正确等)之外,最好对各种失败案例(参数不正确或丢失、数据库连接不可用等)进行测试。我建议 使用 NUnit 在 C# 中进行实用的单元测试 作为参考。

正如其他人所说,除非 getter 和 setter 中有条件逻辑,否则对 getter 和 setter 进行单元测试是多余的。

虽然可以正确猜测代码需要测试的位置,但我通常认为您需要指标来支持这种猜测。在我看来,单元测试与代码覆盖率指标密切相关。

经过大量测试但覆盖范围较小的代码尚未经过充分测试。也就是说,具有 100% 覆盖率但未测试边界和错误情况的代码也不是很好。

您希望在高覆盖率(最低 90%)和可变输入数据之间取得平衡。

记得测试一下“垃圾进来”!

此外,除非检查失败,否则单元测试就不是单元测试。没有断言或标记有已知异常的单元测试将简单地测试代码在运行时不会死掉!

您需要设计您的测试,以便它们始终报告失败或意外/不需要的数据!

它使我们的代码变得更好......时期!

我们软件开发人员在进行测试驱动开发时忘记的一件事是我们行为背后的目的。如果在生产代码已经就位之后编写单元测试,则测试的价值会大幅下降(但不会完全丢失)。

本着单元测试的真正精神,这些测试是 不是 主要是为了“测试”更多我们的代码;或者获得 90%-100% 更好的代码覆盖率。这些都是 附加福利 首先编写测试。最大的回报是,由于 TDD 的自然过程,我们的生产代码最终会写得更好。

为了帮助更好地传达这个想法,以下内容可能会对阅读有所帮助:

有缺陷的单元测试理论
有目的的软件开发

如果我们觉得写作的行为 更多单元测试 是帮助我们获得更高质量的产品,那么我们可能会遭受 货物崇拜 测试驱动开发。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top