简体   繁体   English

使用NUnit的.NET的TDD工作流最佳实践

[英]TDD workflow best-practices for .NET using NUnit

UPDATE: I made major changes to this post - check the revision history for details. 更新:我对此帖进行了重大更改 - 查看修订历史记录以获取详细信息。

I'm starting to dive into TDD with NUnit and despite I've enjoyed checking some resources I've found here at stackoverflow, I often find myself not gaining good traction. 我开始使用NUnit深入研究TDD ,尽管我很喜欢在stackoverflow上查看我在这里找到的一些资源,但我经常发现自己没有获得良好的牵引力。

So what I'm really trying to achieve is to acquire some sort of checklist/workflow —and here's where I need you guys to help me out— or " Test Plan " that will give me decent Code Coverage. 所以我真正想要实现的是获得某种清单/工作流程 - 这就是我需要你们帮助我的地方 - 或者“ 测试计划 ”,它将为我提供合适的代码覆盖率。

So let's assume an ideal scenario where we could start a project from scratch with let's say a Mailer helper class that would have the following code: 因此,让我们假设一个理想的情况,我们可以从头开始一个项目,让我们说一个Mailer助手类,它将具有以下代码:

(I've created the class just for the sake of aiding the question with a code sample so any criticism or advice is encouraged and will be very welcome) (我创建这个课程的目的只是为了通过代码示例帮助解决问题,因此鼓励任何批评或建议,并且非常欢迎)

Mailer.cs Mailer.cs

using System.Net.Mail;
using System;

namespace Dotnet.Samples.NUnit
{
    public class Mailer
    {
        readonly string from;
        public string From { get { return from; } }

        readonly string to;
        public string To { get { return to; } }

        readonly string subject;
        public string Subject { get { return subject; } }

        readonly string cc;
        public string Cc { get { return cc; } }

        readonly string bcc;
        public string BCc { get { return bcc; } }

        readonly string body;
        public string Body { get { return body; } }

        readonly string smtpHost;
        public string SmtpHost { get { return smtpHost; } }

        readonly string attachment;
        public string Attachment { get { return Attachment; } }

        public Mailer(string from = null, string to = null, string body = null, string subject = null, string cc = null, string bcc = null, string smtpHost = "localhost", string attachment = null)
        {
            this.from = from;
            this.to = to;
            this.subject = subject;
            this.body = body;
            this.cc = cc;
            this.bcc = bcc;
            this.smtpHost = smtpHost;
            this.attachment = attachment;
        }

        public void SendMail()
        {
            if (string.IsNullOrEmpty(From))
                throw new ArgumentNullException("Sender e-mail address cannot be null or empty.", from);

            SmtpClient smtp = new SmtpClient();
            MailMessage mail = new MailMessage();
            smtp.Send(mail);
        }
    }
}

MailerTests.cs MailerTests.cs

using System;
using NUnit.Framework;
using FluentAssertions;

namespace Dotnet.Samples.NUnit
{
    [TestFixture]
    public class MailerTests
    {
        [Test, Ignore("No longer needed as the required code to pass has been already implemented.")]
        public void SendMail_FromArgumentIsNotNullOrEmpty_ReturnsTrue()
        {
            // Arrange
            dynamic argument = null;

            // Act
            Mailer mailer = new Mailer(from: argument);

            // Assert
            Assert.IsNotNullOrEmpty(mailer.From, "Parameter cannot be null or empty.");
        }

        [Test]
        public void SendMail_FromArgumentIsNullOrEmpty_ThrowsException()
        {
            // Arrange
            dynamic argument = null;
            Mailer mailer = new Mailer(from: argument);

            // Act
            Action act = () => mailer.SendMail();
            act.ShouldThrow<ArgumentNullException>();

            // Assert
            Assert.Throws<ArgumentNullException>(new TestDelegate(act));
        }

        [Test]
        public void SendMail_FromArgumentIsOfTypeString_ReturnsTrue()
        {
            // Arrange
            dynamic argument = String.Empty;

            // Act
            Mailer mailer = new Mailer(from: argument);

            // Assert
            mailer.From.Should().Be(argument, "Parameter should be of type string.");
        }

        // INFO: At this first 'iteration' I've almost covered the first argument of the method so logically this sample is nowhere near completed.
        // TODO: Create a test that will eventually require the implementation of a method to validate a well-formed email address.
        // TODO: Create as much tests as needed to give the remaining parameters good code coverage.
    }
}

So after having my first 2 failing tests the next obvious step would be implementing the functionality to make them pass, but, should I keep the failing tests and create new ones after implementing the code that will make those pass, or should I modify the existing ones after making them pass? 因此,在我的前两个失败的测试之后,下一个明显的步骤是实现使它们通过的功能,但是,我应该保留失败的测试并在实现将使这些通过的代码之后创建新的测试,或者我应该修改现有的让它们通过之后?

Any advice about this topic will really be enormously appreciated. 关于这个主题的任何建议都会非常感激。

If you install TestDriven.net , one of the components (called NCover) actually helps you understand how much of your code is covered by unit test. 如果您安装TestDriven.net ,其中一个组件(称为NCover)实际上可以帮助您了解单元测试涵盖了多少代码。

Barring that, the best solution is to check each line, and run each test to make sure you've at least hit that line once. 除此之外,最好的解决方案是检查每一行,并运行每个测试以确保您至少打过一次该行。

If you use a framework like NUnit, there are methods available such as AssertThrows where you can assert that a method throws the required exception given the input: http://www.nunit.org/index.php?p=assertThrows&r=2.5 如果您使用像NUnit这样的框架,可以使用AssertThrows这样的方法,在这些方法中,您可以声明方法在输入时抛出所需的异常: httpAssertThrows

Basically, verifying expected behavior given good and bad inputs is the best place to start. 基本上,在给出良好和不良投入的情况下验证预期行为是最佳起点。

I'd suggest that you pick up some tool like NCover which can hook onto your test cases to give code coverage stats. 我建议你选择一些像NCover这样的工具,它可以挂钩你的测试用例来提供代码覆盖率统计。 There is also a community edition of NCover if you don't want the licensed version. 如果您不需要许可版本,还有一个NCover社区版。

When people (finally!) decide to apply test coverage to an existing code base, it is impractical to test everything; 当人们(最终!)决定将测试覆盖率应用于现有代码库时,测试所有内容是不切实际的; you don't have the resources, and there isn't often a lot of real value. 你没有资源,而且往往没有太大的实际价值。

What you ideally want to do is to make sure that your tests apply to newly written/modified code and anything that might be affected by those changes . 理想情况下,您要确保测试适用于新编写/修改的代码以及可能受这些更改影响的任何内容

To do this, you need to know: 要做到这一点,你需要知道:

  • what code you changed. 你改变了什么代码。 Your source control system will help you here at the level of this-file-changed. 您的源代码管理系统将在此文件更改的级别为您提供帮助。

  • what code is executed as a consequence of the new code being executed. 执行新代码后执行的代码是什么。 For this you need either a static analyzer that can trace the downstream impact of the code (don't know of many of these) or a test coverage tool, which can show what has been executed when you run your specific tests. 为此,您需要一个可以跟踪代码的下游影响的静态分析器(不知道其中的许多代码)或测试覆盖率工具,它可以显示运行特定测试时执行的内容。 Any such executed code probably needs re-testing, too. 任何这样执行的代码也可能需要重新测试。

Because you want to minimize the the amount of test code you write, you clearly want better than file-precision granularity of "changed". 因为您希望最小化您编写的测试代码量,所以您显然希望比“已更改”的文件精度粒度更好。 You can use a diff tool (often build into your source control system) to help hone the focus to specific lines. 您可以使用diff工具(通常构建到源控件系统中)来帮助将焦点转移到特定的行。 Diff tools don't actually understand code structure, so what they report tends to be line-oriented rather than structure oriented, producing rather bigger diffs than necessary; 差异工具实际上并不理解代码结构,因此他们报告的内容往往是面向行而不是面向结构,产生比必要更大的差异; nor do they tell you the convenient point of test access, which is likely to be a method because the whole style of unit test is focused on testing methods. 他们也没有告诉你方便的测试访问点,这可能是一种方法,因为整个单元测试的风格都集中在测试方法上。

You can get better diff tools. 你可以得到更好的差异工具。 Our Smart Differencer tools provide differences in terms of program structures (expressions, statements, methods) and abstracting editing operations (insert, delete, copy, move, replace, rename) which can make it easier to interpret the code changes. 我们的Smart Differencer工具提供了程序结构(表达式,语句,方法)和抽象编辑操作(插入,删除,复制,移动,替换,重命名)方面的差异,这使得更容易解释代码更改。 This doesn't directly solve the "which method changed?" 这并没有直接解决“哪种方法改变了?” question, but it often means looking at a lot less stuff to make that decision. 问题,但它通常意味着要做出很少的事情来做出决定。

You can get test coverage tools that will answer this question. 您可以获得将回答此问题的测试覆盖率工具。 Our Test Coverage tools have a facility to compare previous test coverage runs with current test coverage runs, to tell you which tests have to be re-run. 我们的测试覆盖率工具可以将先前的测试覆盖率运行与当前的测试覆盖率运行进行比较,以告诉您必须重新运行哪些测试。 They do so by examining the code differences (something like the Smart Differencer) but abstract the changes back to method level. 他们通过检查代码差异(类似于智能差异器)来做到这一点,但将更改抽象回方法级别。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM