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

C# 使用NUnit的.NET TDD工作流最佳实践,c#,tdd,nunit,code-coverage,C#,Tdd,Nunit,Code Coverage,更新:我对这篇文章做了重大修改-查看修订历史以了解详细信息 我开始用NUnit深入研究TDD,尽管我很喜欢在stackoverflow这里找到一些资源,但我经常发现自己没有获得很好的吸引力 所以,我真正想要实现的是获得某种检查表/工作流程——这里我需要你们的帮助——或者“测试计划”,这将为我提供体面的代码覆盖率 因此,让我们假设一个理想的场景,在这个场景中,我们可以从头开始一个项目,比如说一个Mailer helper类,它将具有以下代码: (我创建这个类只是为了用代码示例帮助回答问题,因此鼓励

更新:我对这篇文章做了重大修改-查看修订历史以了解详细信息

我开始用NUnit深入研究TDD,尽管我很喜欢在stackoverflow这里找到一些资源,但我经常发现自己没有获得很好的吸引力

所以,我真正想要实现的是获得某种检查表/工作流程——这里我需要你们的帮助——或者“测试计划”,这将为我提供体面的代码覆盖率

因此,让我们假设一个理想的场景,在这个场景中,我们可以从头开始一个项目,比如说一个Mailer helper类,它将具有以下代码:

(我创建这个类只是为了用代码示例帮助回答问题,因此鼓励并欢迎任何批评或建议)

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);
        }
    }
}
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.
    }
}
MailerTests.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);
        }
    }
}
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.
    }
}
使用系统;
使用NUnit.Framework;
使用FluentAssertions;
命名空间Dotnet.Samples.NUnit
{
[测试夹具]
公共类邮件测试
{
[测试,忽略(“不再需要,因为需要通过的代码已经实现了。”)]
public void SendMail\u fromagumentisnotnull或empty\u ReturnsTrue()
{
//安排
动态参数=null;
//表演
Mailer-Mailer=new-Mailer(from:argument);
//断言
IsNotNullOrEmpty(mailer.From,“参数不能为null或空”);
}
[测试]
public void SendMail\u FromArgumentIsNullOrEmpty\u ThrowsException()
{
//安排
动态参数=null;
Mailer-Mailer=new-Mailer(from:argument);
//表演
Action act=()=>mailer.SendMail();
应该扔的动作;
//断言
抛出(newtestdelegate(act));
}
[测试]
public void SendMail_FromArgumentIsOfTypeString_ReturnsTrue()
{
//安排
动态参数=String.Empty;
//表演
Mailer-Mailer=new-Mailer(from:argument);
//断言
mailer.From.Should().Be(参数“参数应为字符串类型”);
}
//信息:在第一次“迭代”中,我几乎涵盖了该方法的第一个参数,所以从逻辑上讲,这个示例远未完成。
//TODO:创建一个测试,最终需要实现一个方法来验证格式良好的电子邮件地址。
//TODO:根据需要创建尽可能多的测试,以便为其余参数提供良好的代码覆盖率。
}
}
因此,在我的前两个失败测试之后,下一个明显的步骤是实现使其通过的功能,但是,我应该保留失败的测试并在实现使其通过的代码后创建新的测试,还是应该在使其通过后修改现有的测试


关于此主题的任何建议都将受到极大的赞赏。

如果您使用NUnit这样的框架,那么有一些可用的方法,例如
AssertThrows
,您可以断言,在给定输入的情况下,某个方法会引发所需的异常:

基本上,验证给定好输入和坏输入的预期行为是最好的开始。

如果安装,其中一个组件(称为NCover)实际上可以帮助您了解单元测试覆盖了多少代码


除此之外,最好的解决方案是检查每条线路,并运行每个测试,以确保至少击中该线路一次

我建议您选择一些工具,比如它可以连接到您的测试用例上,以提供代码覆盖率统计数据。如果您不想要许可版本,还可以使用社区版的NCover。

当人们(最终!)决定对现有代码库应用测试覆盖率时,测试所有内容是不切实际的;你没有足够的资源,也没有太多的实际价值

理想情况下,您希望做的是确保您的测试应用于新编写/修改的代码以及可能受这些更改影响的任何内容

为此,您需要知道:

  • 您更改了什么代码。您的源代码管理系统将帮助您在此级别更改此文件

  • 执行新代码的结果是执行什么代码。为此,您需要一个可以跟踪代码下游影响的静态分析器(其中许多是不知道的)或一个测试覆盖率工具,它可以显示在运行特定测试时执行的内容。任何这样执行的代码可能也需要重新测试

因为您希望最小化您编写的测试代码的数量,所以您显然希望“changed”的粒度优于文件精度。您可以使用diff工具(通常内置于源代码管理系统中)来帮助将焦点磨练到特定的行。Diff工具实际上并不理解代码结构,所以他们报告的内容往往是面向行的,而不是面向结构的,产生的差异比需要的要大;他们也没有告诉您方便的测试访问点,这很可能是一种方法,因为单元测试的整个风格都集中在测试方法上

你可以得到更好的diff工具。我们的工具在程序结构(表达式、语句、方法)和抽象编辑操作(插入、删除、复制、移动、替换、重命名)方面提供了差异,这使得解释代码更改变得更容易。这并不能直接解决“哪种方法改变了?”的问题,但这通常意味着在做出决定时要考虑的东西要少得多

您可以使用测试覆盖率工具来回答这个问题。我们的工具有一个功能,可以比较以前的测试覆盖率运行和当前的测试覆盖率运行,告诉您哪些测试必须重新运行。他们通过检查代码差异(比如智能Di)来实现这一点