Answers:
我对此问题也做出了许多很好的回答:“ 开始TDD-挑战?解决方案?建议? ”
我也可以建议您看一下我的博客文章(部分受我的问题启发),对此我得到了很好的反馈。即:
我不知道从哪里开始?
- 重新开始。仅在编写新代码时考虑编写测试。这可以是旧代码的重新设计,也可以是全新的功能。
- 从简单开始。不要逃避,试图让自己绕过一个测试框架以及像TDD这样的风格。Debug.Assert工作正常。使用它作为起点。它不会弄乱您的项目或创建依赖项。
- 开始积极。您正在尝试改善自己的手艺,对此感到满意。我看到很多开发人员乐于停滞不前,不愿意尝试新事物来改善自己。您在做正确的事,请记住这一点,这将有助于阻止您放弃。
- 开始准备挑战。开始进行测试非常困难。期待挑战,但要记住–挑战可以克服。
只测试您的期望
刚开始时,我遇到了真正的问题,因为我一直坐在那里,试图找出可能发生的所有可能的问题,然后尝试进行测试并修复。这是头痛的快速方法。测试应该是真实的YAGNI过程。如果您知道有问题,请为此编写测试。否则,请不要打扰。
只测试一件事
每个测试用例只能测试一件事。如果发现自己在测试用例名称中加上了“ and”,则说明您做错了。
我希望这意味着我们可以从“ getters and setters”继续前进:)
这使我进入了单元测试,这让我感到非常高兴
我们才刚刚开始进行单元测试。很长一段时间以来,我都知道开始这样做会很好,但是我不知道如何开始,更重要的是不知道要测试什么。
然后,我们不得不在会计程序中重写重要的一段代码。这部分非常复杂,因为它涉及许多不同的场景。我正在谈论的部分是一种支付已经输入到会计系统中的销售和/或购买发票的方法。
我只是不知道如何开始编码,因为有很多不同的付款方式。一张发票可能是100美元,但客户只转移了99美元。也许您已经向客户发送了销售发票,但是您也已经从该客户那里购买了发票。因此,您以300美元的价格卖给了他,却以100美元的价格买了他。您可以期望您的客户向您支付$ 200来结清余额。而且,如果您以500美元的价格出售,但客户只付给您250美元,该怎么办?
因此,我有一个非常复杂的问题要解决,有很多可能性,一种方案可以很好地工作,但在另一种报酬/付款组合上却是错误的。
这是拯救单元测试的地方。
我开始(在测试代码内)编写一种方法来创建用于销售和购买的发票清单。然后,我编写了第二种方法来创建实际付款。通常,用户会通过用户界面输入该信息。
然后,我创建了第一个TestMethod,测试了一张发票的非常简单的付款,没有任何付款折扣。当将银行付款保存到数据库中时,系统中的所有操作都会发生。如您所见,我创建了发票,创建了付款(银行交易)并将交易保存到磁盘。在我的断言中,我列出了正确的数字,该数字应该最终出现在银行交易和链接的发票中。我检查交易后的付款次数,付款金额,折扣金额和发票余额。
测试运行后,我将转到数据库并仔细检查是否存在我期望的值。
之后我写的测试,我开始编码的付款方式(在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);
}
Assert.AreEqual(0, bankHeader.BankCashDetails[0].Payments[3].InvoiceHeader.Balance);
这个问题似乎是一个问题,一个人在哪里划定了什么方法被测试,哪些没有被测试。
创建价值分配的设置者和获取者时要考虑到一致性和未来的增长,并且可以预见,在将来的一段时间中,设置者/获取者可能会演变为更复杂的操作。出于一致性和未来发展的考虑,将这些方法的单元测试放在适当的位置也是很有意义的。
主要的目标是代码可靠性,尤其是在进行更改以添加其他功能时。我不知道有人曾因为在测试方法中包含setter / getter而被解雇,但我确定有些人希望他们测试了他们最后知道或可以回忆起的简单的set / get包装器,但是那不是更长的时间。
也许团队的另一名成员扩展了set / get方法,使其包含现在需要测试但无需创建测试的逻辑。但是现在您的代码正在调用这些方法,并且您不知道它们已更改,需要进行深入测试,并且您在开发和质量检查中进行的测试不会触发缺陷,但是发布第一天的实际业务数据确实可以触发它。
现在,这两个队友将辩论谁丢球并未能进行单元测试,而设置/获取的变形包括可能会失败但未包含在单元测试中的逻辑。如果从第一天开始就在简单的“设置/获取”上实施测试,最初编写“设置/获取”的队友将拥有更轻松的时间。
我的观点是,几分钟的“浪费”时间涵盖了所有带有单元测试的方法,甚至是琐碎的方法,这可能会节省几天的头痛,并减少金钱/业务信誉和某人的工作。
而且,当初级合伙人将琐碎的方法更改为非琐碎的方法并提示他们更新测试时,您可能会用普通的方法将琐碎的方法包装到单元测试中,而现在没有人遇到麻烦,因为包含缺陷从生产开始。
我们的编码方式以及可以从我们的代码中看到的纪律可以为他人提供帮助。
诸如getter和setter之类的琐碎真正的代码,除了设置私有字段外,没有其他行为,因此测试起来过于刻薄。在3.0中,C#甚至有一些语法糖,编译器负责处理私有字段,因此您无需进行编程。
我通常会编写许多非常简单的测试来验证我期望从类中获得的行为。即使是简单的事情,例如将两个数字相加。我在编写一个简单的测试和编写一些代码行之间进行了很多切换。原因是我可以围绕代码进行更改,而不必担心我破坏了我没有想到的东西。
您应该测试所有内容。现在您有吸气剂和吸气剂,但是有一天您可能会对其进行一些更改,例如进行验证或其他操作。您今天编写的测试将在明天使用,以确保一切正常进行。编写测试时,您应该忘记诸如“现在很简单”之类的注意事项。在敏捷或测试驱动的上下文中,您应该在假设将来进行重构的情况下进行测试。另外,您是否尝试过输入非常奇怪的值,例如极长的字符串或其他“不良”内容?好吧,您应该……永远不要假设将来会严重滥用您的代码。
总的来说,我发现编写大量的用户测试是很费力的。另一方面,尽管它始终为您提供有关应用程序工作方式的宝贵见解,并帮助您摒弃简单(错误)的假设(例如:用户名的长度始终小于1000个字符)。
为您的getter和setter编写单元测试没有什么坏处。现在,他们可能只是在后台进行字段获取/设置,但是将来您可能具有验证逻辑或需要测试的属性间依赖关系。现在,您在考虑它的时候编写它会容易得多,如果有时间的话记得记住对其进行改造。
如果您认为它可能会损坏,请为此编写一个测试。我通常不测试setter / getter,但是可以说您为User.Name做一个名字和姓氏的连接,我会编写一个测试,以便如果有人更改姓氏和名字的顺序,至少他会知道他改变了一些经过测试的东西。
您必须使用UT覆盖该类的每个方法的执行,并检查方法的返回值。这包括getter和setter,尤其是在member(properties)是复杂的类的情况下,这需要在初始化期间分配大量内存。例如,使用一些非常大的字符串(或者带有希腊符号的东西)调用设置器,并检查结果是否正确(不被截断,编码是否正确等)。
如果简单整数也适用-如果您传递长整数而不是整数怎么办?这就是您为UT编写原因的原因:
理想情况下,您在编写课程时就应该完成单元测试。使用“测试驱动开发”时,这就是您要做的事情。在实现每个功能点时添加测试,并确保也用测试覆盖边缘情况。
之后编写测试要痛苦得多,但可行。
这是我在您的职位上要做的事情:
这应该为您提供了一套不错的单元测试工作集,可以很好地缓冲回归。
这种方法的唯一问题是,必须以这种方式将代码设计为可测试的。如果您在早期就犯了任何耦合错误,那么您将很难轻易获得高覆盖率。
这就是为什么在编写代码之前编写测试非常重要的原因。它迫使您编写松散耦合的代码。
甚至get / set也会产生奇怪的结果,具体取决于它们的实现方式,因此应将它们视为方法。
每个测试都需要为这些属性指定参数集,同时定义可接受和不可接受的属性,以确保调用以预期的方式返回/失败。
您还需要了解安全陷阱,例如SQL注入示例,并进行测试。
因此,是的,您确实需要担心测试属性。
我相信测试吸气剂和设置器只是一个简单的操作,这是愚蠢的。我个人不编写复杂的单元测试来涵盖任何使用模式。我尝试编写足够的测试以确保我已经处理了正常的执行行为以及我能想到的尽可能多的错误情况。我将编写更多的单元测试作为对错误报告的回应。我使用单元测试来确保代码满足要求,并使将来的修改更加容易。当我知道如果我破坏某项测试会失败时,我会更愿意更改代码。
我会为您正在为其编写代码的任何东西编写测试,该测试可以在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),它涉及更多的内容,而不是从单元测试技能入手的好地方。
因此,故事的寓意是,测试任何可能引起您担心的事情,保持单元测试测试较小的特定事物,很多测试都是好的。
将您的业务逻辑放在用户界面层的外面,这样您就可以轻松地为它们编写测试,这会很不错。
我建议将TestDriven.Net或ReSharper都轻松集成到Visual Studio中。
我建议为Authenticate和Save方法编写多个测试。除了成功案例(提供所有参数,所有内容均拼写正确等)之外,最好对各种失败案例进行测试(参数不正确或丢失,适用时不可用的数据库连接等)。我建议使用NUnit在C#中进行实用单元测试作为参考进行。
就像其他人所说的那样,除非对getter和setter有条件逻辑,否则对getter和setter的单元测试是过大的。
尽管可以正确猜测代码需要在哪里进行测试,但我通常认为您需要指标来支持这一猜测。我认为单元测试与代码覆盖率指标紧密相关。
具有大量测试的代码,但覆盖率较小,尚未经过良好的测试。也就是说,覆盖率100%但未测试边界和错误情况的代码也不是很好。
您需要在高覆盖率(最小90%)和可变输入数据之间取得平衡。
记住要测试“垃圾进入”!
同样,除非单元测试检查失败,否则它不是单元测试。没有断言或标记有已知异常的单元测试将仅测试代码在运行时不会消失!
您需要设计测试,以便它们始终报告失败或意外/不需要的数据!
我们软件开发人员在进行测试驱动开发时忘记的一件事是我们行动的目的。如果已经在生产代码就绪后编写了单元测试,则测试的值将下降(但不会完全丢失)。
本着真正的单元测试精神,这些测试并不是主要用来“测试”我们的更多代码;或获得90%-100%更好的代码覆盖率。这些都是首先编写测试的附带好处。最大的收获是,由于TDD的自然过程,我们的生产代码结尾可以更好地编写。
为了更好地传达这种想法,以下内容可能有助于阅读:
如果我们认为编写更多的单元测试的行为可以帮助我们获得更高质量的产品,那么我们可能正在遭受“测试驱动开发” 的货物崇拜。