单元测试:自包含测试与代码重复(DRY)

时间:2009-07-07 06:17:34

标签: c# unit-testing nunit

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

  • 每一个单元测试都应该是独立的,而不是依赖于其他测试。
  • 不要重复自己。

更具体一点,我有一个我想测试的进口商。导入器具有“导入”功能,可以获取原始数据(例如,从CSV中获取)并返回某种类型的对象,该对象也将通过ORM(在本例中为LinqToSQL)存储到数据库中。

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

我的班级看起来像这样:

[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(row); 行,因为它们应该是自包含的。但这确实违反了DRY原则,有可能在某一天导致性能问题。

在这种情况下,最佳做法是什么?

5 个答案:

答案 0 :(得分:5)

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

答案 1 :(得分:3)

这取决于您的测试中常见的场景数量。在博客文章中你提到主要的抱怨是SetUp方法为三个测试做了不同的设置,这不能被认为是最佳实践。在您的情况下,您为每个测试/场景设置了相同的设置,然后您应该使用共享的SetUp而不是在每个测试中复制代码。如果您稍后发现有更多测试不共享此设置或需要在一组测试之间共享不同的设置,则将这些测试重构为新的测试用例类。您也可以使用未使用[SetUp]标记的共享设置方法,但在每个需要它们的测试开始时调用它们:

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

找到正确混合的方法可能是在没有SetUp方法的情况下开始,当你发现你正在为测试设置复制代码然后重构为共享方法。

答案 2 :(得分:2)

你可以把  Job j = csvImporter.ImportJob(row); 在你的设置中。这样你就不会重复代码了。

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

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

答案 3 :(得分:1)

你是否搬家

Job j = csvImporter.ImportJob(row);

是否进入SetUp函数,它仍然会在执行每个测试之前执行。如果在每个测试的顶部都有完全相同的行,那么将该行移动到SetUp部分是合乎逻辑的。

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

答案 4 :(得分:1)

在我的一个项目中,我们同意团队我们不会在单元测试构造函数中实现任何初始化逻辑。我们有Setup,TestFixtureSetup,SetupFixture(自NUnit版本2.4起)属性。当我们需要初始化时,几乎所有情况都足够了。我们强制开发人员使用其中一个属性并明确定义我们是否会在每次测试之前,在fixture中的所有测试之前或命名空间中的所有测试之前运行此初始化代码。

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

我认为这取决于项目,PM或团队负责人需要计划和评估单元测试的质量,完整性和代码覆盖率,就好像他们估计了产品的所有其他业务功能一样。我认为,最好是复制粘贴单元测试覆盖80%的生产代码,然后进行设计精良且分离的单元测试,仅覆盖20%。