为何以及如何在遗留应用程序代码中实现初始单元测试

时间:2011-05-06 00:22:13

标签: c# .net unit-testing refactoring

我正在将单元测试集成到现有的遗留应用程序中。在“使用遗留应用程序”一书和我阅读的许多其他书籍中,有人写道,在开始重构现有代码或集成新功能,纠正错误等过程之前,您应始终编写单元测试...

在我阅读的大量样本中,重构方法的签名从未或很少中断,旧的单元测试在经过大量更改后仍然有效。原因在于,当我使用我认为是“遗留代码”的内容时,我每天查看的代码都不是那么遗留。

实际上,当你有一个遗留应用程序时,代码是如此糟糕,你必须打破方法的签名。如果您尝试使用原始方法编写单元测试,只需更改5分钟后,您将打破整个签名,第一次测试将很好地发送到垃圾箱。

举个例子,请看下面的代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MyCompany.Accouting
{
    public class DataCreator
    {
        public static System.Data.DataSet CreateInvoice(
            System.Data.DataSet customer, 
            System.Data.DataSet order, 
            string mails, 
            ref bool isValid)
        {
            System.Data.DataSet invoice = new System.Data.DataSet();

            int taxGroupId =
                ApplicationException.ShareConnection.ExecuteScalar(
                    "SELECT Id FROM TaxGroup WHERE TaxGroup.IsDefault");

            Application.ShareConnection.ExecuteNonQuery(
                "INSERT INTO Invoice (CustomerId, EffectiveDate) VALUES(?,?)",
                 customer.Tables[0].Rows[0]["Id"], System.DateTime.Now);

            int invoiceId;

            invoiceId = Application.SharedConnection.ExecuteScalar("SELECT @@IDENTITY");

            Application.SharedConnection.ExecuteNonQuery(
                "INSERT INTO InvoiceLine (ProductId, Quantity, InvoiceId) VALUES(?,?,?)", ,
                order.Tables[0].Rows[0]["ProductId"], order.Tables[0].Rows[0]["Quantity"], invoiceId);

            foreach(string mail in mails.Split(';'))
            {
                Application.MailSender.Send(mail);
            }

            isValid = true;

            System.Data.DataRow row = invoice.Tables[0].NewRow();
            row["Id"] = invoiceId;

            invoice.Tables[0].Rows.Add(row);

            return invoice;
        }
    }
}

正如你所看到的,这里有很多不好的代码。

重构后,方法不会是静态的,ref参数将被删除,DataSet将被转换为POCO对象,对“Application”这样的全局对象的访问将被动态注入的属性所取代,并且很多其他更改将会被像实现接口,查看类的名称,命名空间和许多其他东西。事实上,这段代码完全是一个废话,应该扔掉并从头开始重写。

如果我为原始静态方法创建单元测试,那么当删除static关键字以便以更加面向对象的方式使用该类时,测试将立即中断。将DataSet更改为Poco等同样......

为什么要在5分钟内创建一个单元测试,我会丢掉这个测试?这项测试有用吗?

在这种情况下,您会使用哪种策略?

非常感谢。

4 个答案:

答案 0 :(得分:4)

这里的关键项目是选择您实际要进行单元测试的点。在您的情况下,对您要替换的确切方法进行测试没有意义。相反,需要为应用程序中调用方法的每个点创建一个测试,以确保特定功能仍然有效。

原因是,一旦完成了对DataCreator类的重构,您将不得不回到调用它的所有区域并更改它们。在进行更改之前对这些区域进行测试将确保您的功能相同。

见下文:

public class SomeClass {
  public Boolean DoSomething() {
    OtherClass oc = new OtherClass();
    return oc.DoSomethingElse("param1", "param2") == "true";
  }
}


public class OtherClass {
  public String DoSomethingElse(String param1, String param2) {
      // horrible code here which never uses the second parameter

     return "true";
  }
}

在上面的示例中,您可能非常希望重构DoSomethingElse以将返回类型更改为布尔值并消除第二个参数。

所以你首先在SomeClass.DoSomething方法上进行单元测试。然后将OtherClass重构为您的内容,确保DoSomething的最终结果相同。

当然,在这种情况下,您需要确保对每个调用“DoSomethingElse”的内容进行单元测试。

答案 1 :(得分:3)

您的单元测试将始终随着签名更改而改变。解决此问题的最佳方法是设置测试一般行为的单元测试,并首先进行简单的优化。

例如,从优化函数代码本身开始(例如,修复数据访问并将函数拆分为一对。)

然后你可以进入签名重构,但在你做之前,确保使用这个类的组件有基本的期望结果测试,这样你就知道在删除out参数的过程中,你忽略了其中一个依赖于此的类。

在进行重大重构时,您的测试会发生很大变化。有时候只需要进行概念性测试就可以了,所以你可以确保使用重构可用性是相似的,或者你会知道什么测试被弃用,需要在许多其他依赖类中更新。

答案 2 :(得分:1)

按照您希望的方式写出界面,然后针对该界面编写单元测试。

然后从接口调用遗留代码,直到测试通过。

然后根据需要进行重构。

右?

答案 3 :(得分:0)

单元测试将作为活动/生活记录,在您开始更改之前,该方法需要/执行的功能。

在重构方法之后,请将它们视为核对清单,以便在重构之前确保它仍然涵盖了它所涵盖的内容。