为什么以及如何在遗留应用程序代码中实施初始单元测试
我正在将单元测试集成到现有的遗留应用程序中。在《使用遗留应用程序》一书中以及我读过的许多其他书中,都写到,在开始重构现有代码或集成新功能、纠正错误等过程之前,您始终应该编写单元测试
......在我阅读的示例中,重构方法的签名永远不会或很少中断,并且旧的单元测试在经过大量更改后仍然有效。原因是作者代码并不像我每天使用我认为的“遗留代码”时查看的代码那么遗留。
实际上,当您拥有遗留应用程序时,代码非常糟糕,以至于您必须破坏方法的签名。如果您尝试使用原始方法编写单元测试,那么在仅仅 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将被转换为POCOs对象,对“Application”等全局对象的访问将被动态注入的属性取代,并且将进行许多其他更改,如实现接口,回顾类的名称、命名空间和许多其他东西。事实上,这段代码完全是一个垃圾,应该扔掉并从头开始重写。
如果我为原始静态方法创建单元测试,则当删除 static 关键字以以更面向对象的方式使用该类时,测试将立即中断。将 DataSet 更改为 Poco 等也是如此……
如果 5 分钟后我会扔掉这个测试,为什么要创建一个单元测试呢?这次测试有什么帮助?
在这种情况下你会使用哪种策略?
非常感谢。
I’m in the process of integrating unit tests in an existing legacy application. In the book “Working with legacy application” and many other books that I read, it was written that you always should write unit tests before starting the process of refactoring existing code or integrating new features, correct bugs, etc...
In the tons of samples that I read, the signature of refactoring methods is never or rarely breaks and the old unit tests still work after a lot of changes. The reason is that author code is not so legacy that the code that I view each day when I work with what I considered “legacy code”.
In the reality, when you have a legacy application, the code is so bad that you must break the signature of methods. If you try to write unit tests with the original method, after just 5 minutes of changes, you will break the entire signature and the firsts tests will be good to be send to the trash.
Just as an example, look at the code below:
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;
}
}
}
As you can see, there is a lot of lot of bad code here.
After the refactoring, the method will not be static, ref parameter will be removed, DataSet will be converted to POCOs object, access to global object like “Application” will be replaced by properties injected dynamically and a lot of other changes will be made like implementing interface, review the name of class, namespace and many many other things. In fact, this code is totally a crap that should be throw away and rewritten from scratch.
If I create a unit tests for the original static method, the test will be break immediately when the static keyword will be removed to use the class in a more object oriented manner. Same for the change of DataSet to Poco, etc…
Why create a unit test if in 5 minutes, I will throw away this test? What in this test is helpful?
Which strategy will you use in this case?
Thank you very much.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
这里的关键是选择您实际上要进行单元测试的点。就您而言,对您要替换的确切方法进行测试是没有意义的。相反,需要为应用程序中调用您的方法的每个点创建测试,以确保特定功能仍然正常工作。
原因是,一旦完成 DataCreator 类的重构,您将必须返回到调用它的所有区域并更改它们。在进行更改之前对这些区域进行测试将确保您的功能相同。
请参见下文:
在上面的示例中,您可能非常希望重构
DoSomethingElse
以将返回类型更改为布尔值并消除第二个参数。因此,您首先对
SomeClass.DoSomething
方法进行单元测试。然后将OtherClass
重构为您心中的内容,确保DoSomething
的最终结果是相同的。当然,在这种情况下,您需要确保对调用“DoSomethingElse”的每个事物都进行单元测试。
The key item here is to pick the point that you are actually going to unit test. In your case, putting a test on the exact method you are replacing doesn't make sense. Instead a test needs to be created for every point in the application that calls your method to ensure that the specific functionality still works the same.
The reason is that once you have completed refactoring the DataCreator class you will have to go back to all of the areas that call it and change those. Putting tests on those areas prior to making changes will ensure that your functionality is the same.
See below:
In the above example, you might very well want to refactor
DoSomethingElse
to change the return type to a boolean value and eliminate the second parameter.So you start by putting a unit test on the
SomeClass.DoSomething
method. Then refactor theOtherClass
to your hearts content making sure the end result ofDoSomething
is the same.Of course, in this situation, you want to make sure you have a unit test for every single thing that calls "DoSomethingElse".
您的单元测试始终必须随着签名的更改而更改。解决这个问题的最佳方法是设置单元测试来测试一般行为,并首先进行简单的优化。
例如,从优化函数代码本身开始(例如,修复数据访问并将函数分成几个函数。)
然后您可以进行签名重构,但在此之前,请确保使用此函数的组件类具有基本的预期结果测试,因此您知道在删除 out 参数的过程中是否忽略了依赖于此的类之一中的某些内容。
当进行重大重构时,您的测试将会发生很大的变化。有时,布置概念测试就足够了,这样您就可以确保通过重构,可用性是相似的,或者您将知道哪些测试已被弃用,哪些测试需要在许多其他依赖类中进行更新。
Your unit tests will always have to change with signature changes. The best way to go about this is to set up unit tests that test general behavior, and do simple optimizations first.
For instance, start with optimizing the function's code itself (for instance, fixing up the data access & splitting the function up into a couple.)
Then you can move onto signature refactoring, but before you do, make sure the components that use this class have basic expected-results tests so you know if in the process of removing the out parameter, you neglected something in one of the classes that depends on this.
When doing major refactoring your tests will change quite a bit. Sometimes it's enough to have the conceptual tests laid out so you can make sure that with the refactor the usability is similar or you'll know by what tests get deprecated, what needs to be updated in many other dependent classes.
按照您想要的方式编写接口,并针对该接口编写单元测试。
然后从接口调用遗留代码,直到测试通过。
然后根据需要进行重构。
正确的?
Write out the interface the way you want it to be, and write the unit tests against that.
Then call the legacy code from the interface until the tests pass.
Then refactor as needed.
Right?
在开始更改方法之前,单元测试将作为该方法需要/执行哪些功能的活动/实时记录。
将它们视为检查表,供您在重构方法后进行思考,以确保它仍然涵盖重构之前所涵盖的内容。
The Unit Test will serve as an active/living record of what functionality was required/performed by the method before you began changing it.
Consider them like checklists for you to think about after you've refactored the method to ensure it still covers what it covered before you refactored it.