C# 单元测试:自包含测试与代码复制(干式)

C# 单元测试:自包含测试与代码复制(干式),c#,unit-testing,nunit,C#,Unit Testing,Nunit,我正在进行单元测试的第一步,我不确定两种在单元测试中似乎相互矛盾的范式,即: 每个单元测试都应该是独立的,而不是依赖于其他单元测试 不要重复你自己 更具体地说,我有一个进口商,我想测试一下。导入器具有“导入”功能,可获取原始数据(如CSV中的数据)并返回某种类型的对象,该对象也将通过ORM(本例中为LinqToSQL)存储到数据库中 现在我想测试一些东西,例如,返回的对象不为null,它的必填字段不为null或空,并且它的属性具有正确的值。我为此编写了3个单元测试。每个测试应该导入并获得作业

我正在进行单元测试的第一步,我不确定两种在单元测试中似乎相互矛盾的范式,即:

  • 每个单元测试都应该是独立的,而不是依赖于其他单元测试
  • 不要重复你自己
更具体地说,我有一个进口商,我想测试一下。导入器具有“导入”功能,可获取原始数据(如CSV中的数据)并返回某种类型的对象,该对象也将通过ORM(本例中为LinqToSQL)存储到数据库中

现在我想测试一些东西,例如,返回的对象不为null,它的必填字段不为null或空,并且它的属性具有正确的值。我为此编写了3个单元测试。每个测试应该导入并获得作业,还是属于一般设置逻辑?另一方面,就我的理解而言,后者将是一个坏主意。而且,这不会违反自我约束吗

我的班级是这样的:

[TestFixture]
public class ImportJob
{
    private TransactionScope scope;
    private CsvImporter csvImporter;

    private readonly string[] row = { "" };

    public ImportJob()
    {
        CsvReader reader = new CsvReader(new StreamReader(
                    @"C:\SomePath\unit_test.csv", Encoding.Default),
                    false, ';');
        reader.MissingFieldAction = MissingFieldAction.ReplaceByEmpty;

        int fieldCount = reader.FieldCount;
        row = new string[fieldCount];

        reader.ReadNextRecord();
        reader.CopyCurrentRecordTo(row);
    }

    [SetUp]
    public void SetUp()
    {
        scope = new TransactionScope();
        csvImporter = new CsvImporter();
    }

    [TearDown]
    public void TearDown()
    {
        scope.Dispose();
    }

    [Test]
    public void ImportJob_IsNotNull()
    {
        Job j = csvImporter.ImportJob(row);

        Assert.IsNotNull(j);
    }

    [Test]
    public void ImportJob_MandatoryFields_AreNotNull()
    {
        Job j = csvImporter.ImportJob(row);

        Assert.IsNotNull(j.Customer);
        Assert.IsNotNull(j.DateCreated);
        Assert.IsNotNull(j.OrderNo);
    }

    [Test]
    public void ImportJob_MandatoryFields_AreValid()
    {
        Job j = csvImporter.ImportJob(row);
        Customer c = csvImporter.GetCustomer("01-01234567");

        Assert.AreEqual(j.Customer, c);
        Assert.That(j.DateCreated.Date == DateTime.Now.Date);
        Assert.That(j.OrderNo == row[(int)Csv.RechNmrPruef]);

    }

    // etc. ...
}
可以看到,我正在执行一行
Job j=csvImporter.ImportJob(行);
在每个单元测试中,因为它们应该是独立的。但这确实违反了DRY原则,并且有一天可能会导致性能问题


这种情况下的最佳实践是什么?

您的测试类与通常的类没有什么不同,应该这样对待:所有良好实践(干式、代码重用等)也应该应用于此。

无论您是否移动

Job j = csvImporter.ImportJob(row);
无论是否进入设置函数,在执行每个测试之前仍将执行该函数。如果在每个测试的顶部都有完全相同的行,那么将该行移到设置部分是合乎逻辑的

您发布的博客文章抱怨说,测试值的设置是在一个与测试本身断开连接的函数中完成的(可能不在同一屏幕上)——但您的情况不同,因为测试数据是由外部文本文件驱动的,因此,该投诉也与您的特定用例不匹配。

您可以将 作业j=csvImporter.ImportJob(行); 在您的设置中。这样您就不会重复代码

实际上,您应该为每个测试运行这行代码。否则,由于其他测试中发生的事情,测试将开始失败。这将变得难以维持


性能问题不是由干冲突引起的。实际上,您应该为每个测试设置所有内容。这些不是单元测试,它们是集成测试,您依赖外部文件来运行测试。您可以使ImportJob从流中读取,而不是直接打开文件。然后,您可以使用memorystream进行测试。

这取决于您的测试中有多少场景是通用的。你在博客中提到的主要投诉是,设置方法对三个测试的设置不同,这不能被认为是最佳实践。在您的情况下,每个测试/场景都有相同的设置,然后您应该使用共享设置,而不是在每个测试中复制代码。如果您稍后发现有更多的测试不共享此设置,或者需要在一组测试之间共享不同的设置,则将这些测试重构为新的测试用例类。您还可以拥有未标记为[setup]但在每个需要它们的测试开始时被调用的共享安装方法:

[Test]
public void SomeTest()
{
   setupSomeSharedState();
   ...
}

找到正确组合的一种方法是,一开始不使用设置方法,当您发现您正在为测试设置复制代码时,然后重构为共享方法。

在我的一个项目中,我们与团队达成一致,即我们不会在单元测试构造函数中实现任何初始化逻辑。我们有Setup、TestFixtureSetup、SetupFixture(从NUnit的2.4版开始)属性。当我们需要初始化时,它们对于几乎所有情况都足够了。我们强制开发人员使用这些属性中的一个,并显式定义是在每次测试之前、在夹具中的所有测试之前还是在命名空间中的所有测试之前运行此初始化代码

然而,我不同意单元测试应该始终确认常规开发中的所有良好实践。这是可取的,但不是规则。我的观点是,在现实生活中,客户不会为单元测试付费。客户为产品的整体质量和功能付费。他不想知道您是通过单元测试/自动化GUI测试覆盖100%的代码,还是通过每个开发人员雇佣3名手动测试人员,在每次构建后单击屏幕的每一部分,从而为他提供一个无缺陷的产品。 单元测试不会为产品增加业务价值,它允许您节省开发和测试工作,并迫使开发人员编写更好的代码。所以这总是取决于你——你会花更多的时间在UT重构上以使单元测试变得完美吗?或者,您是否会花同样的时间为您产品的客户添加新功能?也不要忘记单元测试应该尽可能简单。如何找到黄金分割


我认为这取决于项目,PM或团队领导需要计划和评估单元测试的质量、完整性和代码覆盖率,就像他们评估产品的所有其他业务特性一样。我的观点是,最好是使用覆盖80%生产代码的复制粘贴单元测试,而不是使用设计良好且仅覆盖20%的独立单元测试。

我已声明“row”字段为只读,这意味着我必须在构造函数中对其进行初始化。我希望它保持只读,因为这样可以避免任何测试修改“行”的危险。如何解决这个问题?您可以在声明变量private readonly string[]row={your values here}时初始化它;此外,您可能根本没有私有字段。为什么不为不同的tes设置不同的局部变量行