我正在将单元测试集成到现有的遗留应用程序中。在“使用遗留应用程序”一书和我阅读的许多其他书籍中,有人写道,在开始重构现有代码或集成新功能,纠正错误等过程之前,您应始终编写单元测试...
在我阅读的大量样本中,重构方法的签名从未或很少中断,旧的单元测试在经过大量更改后仍然有效。原因在于,当我使用我认为是“遗留代码”的内容时,我每天查看的代码都不是那么遗留。
实际上,当你有一个遗留应用程序时,代码是如此糟糕,你必须打破方法的签名。如果您尝试使用原始方法编写单元测试,只需更改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分钟内创建一个单元测试,我会丢掉这个测试?这项测试有用吗?
在这种情况下,您会使用哪种策略?
非常感谢。
答案 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)
单元测试将作为活动/生活记录,在您开始更改之前,该方法需要/执行的功能。
在重构方法之后,请将它们视为核对清单,以便在重构之前确保它仍然涵盖了它所涵盖的内容。