在编写unit testing时,你怎么知道要testing什么?
使用C#,我需要一个名为User
的类,它具有用户名,密码,活动标志,名字,姓氏,全名等。
应该有方法来validation和保存用户。 我只是写一个testing的方法? 我甚至需要担心testing属性,因为它们是.Net的getter和setter?
我的问题也有很多很好的回应:“ 开始TDD – 挑战?解决scheme?build议?
我也可以推荐看看我的博客文章 (部分原因是我的问题),对此我有一些很好的反馈意见。 即:
我不知道从哪里开始?
- 重新开始。 只有在编写新代码时才考虑编写testing。 这可以重新使用旧的代码,或者一个全新的function。
- 开始简单。 不要去跑掉,试图让你的头在一个testing框架,以及TDD式的。 Debug.Assert工作正常。 用它作为起点。 它不会混淆你的项目或创build依赖项。
- 开始积极。 你正在努力改进你的手艺,感觉很好。 我看到很多开发者都乐于停滞不前,不尝试新的东西来改善自己。 你做正确的事情,记住这一点,这将有助于阻止你放弃。
- 开始准备迎接挑战。 开始进入testing是相当困难的。 期待一个挑战,但要记住 – 挑战是可以克服的。
只testing你的期望
我刚开始的时候遇到了一些实际的问题,因为我经常坐在那里试图找出每一个可能发生的问题,然后试着去testing和修复。 这是一个快速的头痛方式。 testing应该是一个真正的YAGNI过程。 如果你知道有问题,那么写一个testing。 否则,不要打扰。
只testing一件事
每个testing用例只能testing一件事情。 如果你发现自己把“和”放在testing用例名称中,那么你做错了什么。
我希望这意味着我们可以从“getters and setters”开始:)
testing你的代码,而不是语言。
unit testing如下:
Integer i = new Integer(7); assert (i.instanceOf(integer));
只有在编写编译器的instanceof
才有用,并且您的instanceof
方法不能正常工作。
不要testing你可以依赖的语言强制执行的东西。 在你的情况下,我会专注于你的身份validation和保存方法 – 我会写testing,确保他们可以在任何或所有这些字段中正常处理空值。
这让我进入了unit testing,这让我非常开心
我们刚开始做unit testing。 很长时间以来,我知道开始这样做是件好事,但我不知道如何开始,更重要的是要testing什么。
然后,我们不得不在会计程序中重写一段重要的代码。 这部分是非常复杂的,因为它涉及到很多不同的场景。 我所说的部分是一种支付已经进入会计系统的销售和/或购买发票的方法。
我只是不知道如何开始编码,因为有很多不同的付款方式。 发票可能是100美元,但客户只转让99美元。 也许你已经发送销售发票给客户,但你也从这个客户购买。 所以你卖了他300美元,但你买了100美元。 您可以期望您的客户支付200美元来结清余额。 而如果你以$ 500的价格出售,但顾客只需支付250美元呢?
所以我有一个非常复杂的问题来解决,有很多可能性,一个场景完全可以工作,但是在其他types的invocie / payment组合上会是错误的。
这是unit testing来救援的地方。
我开始写(在testing代码中)一种方法来创build发票清单,无论是销售还是购买。 然后我写了第二种方法来创build实际的付款。 通常,用户将通过用户界面input该信息。
然后,我创build了第一个TestMethod,testing一个非常简单的单一发票支付没有任何付款折扣。 当银行支付将被保存到数据库时,系统中的所有动作都会发生。 如您所见,我创build了一张发票,创build了一笔付款(银行交易)并将交易保存到磁盘。 在我的断言中,我把什么是正确的数字结束在银行交易和链接的发票。 我检查交易后的付款次数,付款金额,折扣金额和发票余额。
在testing运行后,我会去数据库,并仔细检查,如果我的预期是在那里。
在我编写testing之后,我开始编写付款方法(BankHeader类的一部分)。 在编码方面,我只是用代码来做第一次testing。 我还没有考虑其他更复杂的情况。
我跑了第一个testing,修正了一个小错误,直到我的testing通过。
然后,我开始写第二个testing,这次使用付款折扣。 在我写testing之后,我修改了付款方式来支持折扣。
当用付款折扣testing正确性时,我也testing了简单付款。 当然这两个testing都应该通过。
然后,我按照更复杂的方式工作。
1)想想一个新的场景
2)为该场景写一个testing
3)运行单个testing,看看它是否会通过
4)如果没有,我会debugging和修改代码,直到它通过。
5)修改代码时,我一直在运行所有的testing
这就是我如何创build我非常复杂的付款方式。 没有unit testing,我不知道如何开始编码,问题似乎压倒一切。 通过testing,我可以从一个简单的方法开始,一步一步地扩展它,保证更简单的scheme仍然有效。
我确信使用unit testing为我节省了几天(或几周)的编码,并且或多或less地保证了我的方法的正确性。
如果我后来想到一个新的场景,我可以把它添加到testing中,看看它是否工作。 如果没有,我可以修改代码,但仍然可以确保其他scheme仍然正常工作。 这将在维护和错误修复阶段节省几天和几天的时间。
是的,即使testing代码仍然可以有错误,如果用户做你没有想到或阻止他做的事情
以下是我创build的一些testing我的付款方式的testing。
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); }
如果他们真的很微不足道,那就不要麻烦testing。 例如,如果他们这样执行;
public class User { public string Username { get; set; } public string Password { get; set; } }
另一方面,如果你正在做一些聪明的事情(比如在getter / setter中encryption和解密密码),那就给它一个testing。
规则是你必须testing你写的每一个逻辑。 如果你在getter和setter中实现了一些特定的function,我认为它们值得testing。 如果他们只给一些私人领域赋值,不要麻烦。
这个问题似乎是一个什么样的方法来testing什么方法,哪些方法没有testing的问题。
价值分配的制定者和获取者已经被创造出来,一致性和未来的增长,并且预见到一段时间以来,制定者/获取者可能演变成更复杂的操作。 对这些方法进行unit testing是有意义的,也是为了一致性和未来的发展。
代码可靠性,特别是在进行更改以添加附加function时,是主要目标。 我没有意识到任何人在testing方法学中都被解雇了,但是我确信有人希望他们testing了最后知道的方法,或者可以回想起简单的set / get wrappers,但是那是不行的更长的情况。
也许团队的另一个成员扩大了set / get方法,以包括现在需要testing但是没有创buildtesting的逻辑。 但是现在,您的代码正在调用这些方法,而您并不知道它们已经发生变化,需要进行深入的testing,而且您在开发和QA中所做的testing不会触发这个缺陷,而是在发布的第一天真正的业务数据触发它。
这两名队友现在将辩论谁投丢了球,并且当这个组合变得包含可能失败但未被unit testing覆盖的逻辑时,他们没有进行unit testing。 最初编写set / get的队友如果从简单的set / gets中的第一天开始执行testing,将会有更简单的时间。
我的意见是,用unit testing覆盖所有方法的“浪费”时间,甚至是微不足道的几分钟,都可能节省头痛的日子,损失业务的资金/声誉和失去工作。
事实上,你用unit testing来包装微不足道的方法,可能会被那些初级队友看到,当他们把微不足道的方法变成非平凡的方法并提示他们更新testing时,现在没有人因为缺陷被包含在内而陷入麻烦从达到生产。
我们编写代码的方式以及从我们的代码中可以看到的规则可以帮助其他人。
另一个典型的答案。 我相信,这来自Ron Jeffries:
只testing你想要工作的代码。
testing样板代码是浪费时间,但正如Slavo所说,如果您为getters / setter添加副作用,那么您应该编写一个testing以配合该function。
如果你正在进行testing驱动的开发,那么你应该先写合同(例如接口),然后编写testing来运行那个logging预期结果/行为的接口。 然后自己编写你的方法,而不用在你的unit testing中接触代码。 最后,获取代码覆盖率工具,并确保您的testing在您的代码中执行所有的逻辑path。
真正微不足道的代码,比如getter和setter,没有比设置私有字段更多的行为,都是过度testing。 在3.0 C#甚至有一些语法糖,编译器负责pipe理私有字段,所以你不必编程。
我通常会写很多非常简单的testing来validation我所期望的课程行为。 即使是简单的东西,如添加两个数字。 我写了一个简单的testing和写一些代码行之间切换很多。 这样做的原因是我可以改变代码而不用害怕我打破了我没有想到的事情。
你应该testing一切。 现在你有getters和setter,但有一天你可能会改变他们,也许做validation或别的东西。 你今天写的testing将在明天使用,以确保一切照常工作。 当你写testing的时候,你应该忘记像“现在这很平凡”的考虑。 在敏捷或testing驱动的环境中,您应该testing假设未来的重构。 另外,你是否尝试了一些非常奇怪的值,如极长的string,或其他“坏”的内容? 那么你应该…从来不假设你的代码在未来会被滥用多么严重。
一般来说,我发现编写大量的用户testing是一方面耗尽。 另一方面,虽然它总是给你非常宝贵的见解,说明你的应用程序应该如何工作,并帮助你放弃容易(和错误)的假设(如:用户名长度总是less于1000个字符)。
对于可能以工具包或开源types的项目为例的简单模块,应该尽可能地testing,包括简单的getter和setter。 你要记住的事情是,当你编写一个特定的模块时,生成一个unit testing是非常简单直接的。 添加getter和setter是最简单的代码,可以毫不费力地处理。 然而,一旦你的代码被放置在一个更大的系统中,这个额外的工作可以保护你免受底层系统的变化,比如基类中的types改变。 testingeverthing是完成回归的最好方法。
为你的getter和setter编写unit testing并没有什么坏处。 现在,他们可能只是在底层进行字段get / set,但是将来您可能会有validation逻辑或需要testing的属性间依赖关系。 现在写这个文件比较容易,但是如果时间到了的话,记住要改进它。
一般来说,只有为某些值定义一个方法时,testing可接受边界上的值。 换句话说,确保你的方法做它应该做的, 但没有更多 。 这很重要,因为当你要失败的时候,你想早点失败。
在inheritance层次结构中,确保testingLSP合规性。
testing默认的getter和setter对我来说似乎不是很有用,除非你打算稍后做一些validation。
正如我理解敏捷开发中的unit testing一样,Mike,是的,你需要testinggetter和setter(假设它们是公开可见的)。 unit testing的整个概念就是将这个课堂上的软件单元作为一个黑盒来testing 。 由于getter和setter是外部可见的,所以你需要对它们进行validation和保存。
如果Authenticate和Save方法使用这些属性,那么你的testing将间接地触及这些属性。 只要属性只是提供对数据的访问,那么显式testing就不是必须的(除非你要100%覆盖)。
我会testing你的getter和setter。 根据谁在编写代码,有些人改变了getter / setter方法的含义。 我已经看到variables初始化和其他validation作为getter方法的一部分。 为了testing这种事情,你需要明确地覆盖这个代码的unit testing。
就个人而言,我会“testing任何可以破解的东西”,简单的getter(甚至更好的自动属性)不会中断。 我从来没有一个简单的返回声明失败,因此从来没有testing过他们。 如果获得者有计算他们或其他forms的陈述,我肯定会为他们添加testing。
就我个人而言,我使用Moq作为模拟对象框架,然后validation我的对象按照应该的方式调用周围的对象。
你必须用UT覆盖类的每个方法的执行,并检查方法的返回值。 这包括getter和setter,特别是在成员(属性)是复杂类的情况下,在初始化期间需要大量的内存分配。 用一些非常大的string来调用setter(或者用希腊符号),并检查结果是否正确(不被截断,编码是否正确等)
在简单整数的情况下,也适用 – 如果你通过长而不是整数? 这就是你写UT的原因:)
我不会testing属性的实际设置。 我会更关心这些属性是如何被消费者填充的,以及它们是如何填充的。 通过任何testing,您必须权衡风险与testing的时间/成本。
尽可能使用unit testing来testing“每一个不重要的代码块”。
如果你的属性是微不足道的,并且不太可能有人会引入一个bug,那么不应该unit testing它们。
您的Authenticate()和Save()方法看起来很适合testing。
理想情况下,当你在写课程时,你会完成你的unit testing。 这就是你使用testing驱动开发时的意图。 您在实现每个function点时添加testing,确保您也使用testing来覆盖边缘案例。
事后写testing更加痛苦,但是可行。
以下是我所要做的:
- 编写一套testing核心function的基本testing。
- 获取NCover并在您的testing中运行它。 目前您的testing覆盖率可能会在50%左右。
- 不断添加覆盖边缘案例的testing,直到覆盖率达到80%-90%
这应该给你一个很好的unit testing工作集,将作为一个良好的缓冲回避。
这种方法唯一的问题是代码必须被devise成可以用这种方式来testing。 如果您早期发现任何耦合错误,您将无法很容易地获得高覆盖率。
这就是为什么在编写代码之前编写testing真的很重要。 它迫使你编写松散耦合的代码。
不要testing明显的工作(样板)代码。 所以如果你的setter和getter只是“propertyvalue = value”和“return propertyvalue”,那么testing它就没有意义了。
即使得到/设置也可能会有一些奇怪的后果,这取决于它们是如何实现的,所以它们应该被视为方法。
每个testing都需要为属性指定一组参数,定义可接受的属性和不可接受的属性,以确保调用以预期的方式返回/失败。
您还需要了解安全性问题,例如SQL注入,并对这些进行testing。
所以是的,你需要担心testing的属性。
我相信,当他们只做一个简单的操作时,testinggetter和setter是愚蠢的。 就个人而言,我不写复杂的unit testing来覆盖任何使用模式。 我尝试编写足够的testing来确保我已经处理了正常的执行行为,以及我能想到的尽可能多的错误情况。 我会写更多的unit testing作为对错误报告的回应。 我使用unit testing,以确保代码符合要求,并使未来的修改更容易。 当我知道如果我打破某些testing将会失败,我感觉更愿意更改代码。
我会为任何你正在编写代码的testing写一个testing,这是在GUI界面之外testing的。
通常情况下,我写的任何逻辑都具有任何业务逻辑,我将其放置在另一个层或业务逻辑层中。
然后,编写testing任何事情是很容易做的事情。
首先,在“业务逻辑层”中为每个公共方法编写unit testing。
如果我有这样的课程:
public class AccountService { public void DebitAccount(int accountNumber, double amount) { } public void CreditAccount(int accountNumber, double amount) { } public void CloseAccount(int accountNumber) { } }
在我写这些代码之前,我会做的第一件事就是开始编写unit testing。
[TestFixture] public class AccountServiceTests { [Test] public void DebitAccountTest() { } [Test] public void CreditAccountTest() { } [Test] public void CloseAccountTest() { } }
编写你的testing来validation你写的代码来做些什么。 如果你迭代了一系列的东西,并且改变了每个东西,写一个testing,做同样的事情,断言实际发生。
还有很多其他的方法可以采用,比如Behaviors Driven Development(BDD),这个方法涉及到更多,而不是一个开始使用unit testing技能的好地方。
所以,故事的寓意是,testing任何你可能担心的事情,保持unit testingtesting小规模的特定事物,很多testing都是好的。
保持你的业务逻辑不在用户界面层,以便你可以轻松地为他们编写testing,而且你会很好。
我build议TestDriven.Net或ReSharper都可以很容易地集成到Visual Studio中。
好,如果你认为它可以打破,写一个testing。 我通常不testingsetter / getter,但是让我们说你为User.Name创build一个连接名字和姓氏的文件,我会写一个testing,所以如果有人改变最后一个名字的顺序,至less他会知道他改变了一些经过testing的东西。
规范的答案是“testing任何可能会中断的事情”。 如果您确定这些属性不会中断,请不要testing它们。
一旦发现有问题(你发现一个bug),显然这意味着你需要testing它。 编写一个testing来重现错误,观察错误,然后修复错误,然后观察testing通过。
我会build议为您的validation和保存方法写多个testing。 除了成功案例(提供所有参数,所有参数都拼写正确等)之外,对各种故障情况(不正确或缺失的参数,不适用的数据库连接(如果适用)等)进行testing也是很好的select。 我推荐使用NUnit的C#中的语义unit testing作为参考。
正如其他人所说的,除非你的getter和setter有条件逻辑,否则对getter和setter的unit testing是过度的。
虽然有可能正确地猜测你的代码需要testing的地方,但我通常认为你需要指标来支持这个猜测。 在我看来,unit testing与代码覆盖率指标是一致的。
代码有很多的testing,但覆盖面小,没有得到很好的testing。 也就是说,覆盖率为100%但不testing边界和错误情况的代码也不是很好。
您希望在高覆盖率(最低90%)和可变input数据之间达到平衡。
记得要testing“垃圾进”!
此外,除非检查失败,否则unit testing不是unit testing。 没有断言或标有已知exception的unit testing将简单地testing代码在运行时不会死亡!
您需要devisetesting,以便始终报告故障或意外/不需要的数据!
它使我们的代码更好…期间!
软件开发人员在做testing驱动开发时忘记的一件事就是我们的行为背后的目的。 如果在生产代码已经存在的情况下正在编写unit testing,则testing值会降低(但不会完全丢失)。
按照真正的unit testing精神,这些testing主要不是为了“testing”更多的代码; 或者获得更好的代码覆盖率90%-100%。 这些都是首先编写testing的附带好处 。 The big payoff is that our production code ends be be written much better due to the natural process of TDD.
To help better communicate this idea, the following may be helpful in reading:
The Flawed Theory of Unit Tests
Purposeful Software Development
If we feel that the act of writing more unit tests is what helps us gain a higher quality product, then we may be suffering from a Cargo Cult of Test Driven Development.