具有文件系统依赖性的 TDD
我有一个集成测试LoadFile_DataLoaded_Successively()。我想将其重构为单元测试,以打破与文件系统的依赖关系。
PS 我是 TDD 新手:
这是我的生产类:
public class LocalizationData
{
private bool IsValidFileName(string fileName)
{
if (fileName.ToLower().EndsWith("xml"))
{
return true;
}
return false;
}
public XmlDataProvider LoadFile(string fileName)
{
if (IsValidFileName(fileName))
{
XmlDataProvider provider =
new XmlDataProvider
{
IsAsynchronous = false,
Source = new Uri(fileName, UriKind.Absolute)
};
return provider;
}
return null;
}
}
和我的测试类(Nunit)
[TestFixture]
class LocalizationDataTest
{
[Test]
public void LoadFile_DataLoaded_Successfully()
{
var data = new LocalizationData();
string fileName = "d:/azeri.xml";
XmlDataProvider result = data.LoadFile(fileName);
Assert.IsNotNull(result);
Assert.That(result.Document, Is.Not.Null);
}
}
知道如何重构它以打破文件系统依赖性
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(11)
你在这里缺少的是控制反转。例如,您可以将依赖项注入原理引入代码中:
在上面的代码中,使用
IXmlDataProviderFactory
接口抽象出XmlDataProvider
的创建。可以在 LocalizationData 的构造函数中提供该接口的实现。现在,您可以按如下方式编写单元测试:FakeXmlDataProviderFactory
如下所示:现在,在您的测试环境中,您可以(并且可能应该)始终手动创建被测类。但是,您希望在工厂方法中抽象创建,以防止在被测类发生更改时必须更改许多测试。
然而,在您的生产环境中,当您必须手动创建类时,它很快就会变得非常麻烦。特别是当它包含许多依赖项时。这就是 IoC/DI 框架的闪光点。他们可以帮助您。例如,当您想在生产代码中使用
LocalizationData
时,您可以编写如下代码:请注意,我正在使用 通用服务定位器为例。
该框架将负责为您创建该实例。然而,使用这样的依赖注入框架,您必须让框架知道您的应用程序需要哪些“服务”。例如,当我使用 Simple Service Locator 库作为示例(即无耻插件)时,您的配置可能如下所示:
此代码通常位于应用程序的启动路径中。当然,这个难题唯一缺少的部分是实际的
ProductionXmlDataProviderFactory
类。就是这样:还请注意,您可能不想自己在生产代码中更新
LocalizationData
,因为此类可能被依赖于此类型的其他类使用。您通常要做的是要求框架为您创建最顶层的类(例如实现完整用例的命令)并执行它。我希望这有帮助。
What you're missing here is inversion of control. For instance, you can introduce the dependency injection principle into your code:
In the code above the creation of the
XmlDataProvider
is abstracted away using anIXmlDataProviderFactory
interface. An implementation of that interface can be supplied in the constructor of the LocalizationData. You can now write your unit test as follows:The
FakeXmlDataProviderFactory
looks like this:Now in your test environment you can (and probably should) always create the class under test manually. However, you want to abstract the creation away in factory methods to prevent you having to change many tests when the class under test changes.
In your production environment however, it can become very cumbersome very soon when you manually have to create the class. Especially when it contains many dependencies. This is where IoC / DI frameworks shine. They can help you with this. For instance, when you want to use the
LocalizationData
in your production code, you might write code like this:Note that I'm using the Common Service Locator as an example here.
The framework will take care of the creation of that instance for you. Using such a dependency injection framework however, you will have to let the framework know which 'services' your application needs. For instance, when I use the Simple Service Locator library as an example (shameless plug that is), your configuration might look like this:
This code will usually go in the startup path of your application. Of course the only missing piece of the puzzle is the actual
ProductionXmlDataProviderFactory
class. Here is it:Please also not that you will probably don't want to new up your
LocalizationData
in your production code yourself, because this class is probably used by other classes that depend on this type. What you would normally do is ask the framework to create the top most class for you (for instance the command that implements a complete use case) and execute it.I hope this helps.
这里的问题是你没有进行 TDD。您首先编写了生产代码,现在您想要测试它。
删除所有代码并重新开始。首先编写测试,然后编写通过该测试的代码。然后写下一个测试等等。
你的目标是什么?给定一个以“xml”结尾的字符串(为什么不是“.xml”?),您需要一个基于名称为该字符串的文件的 XML 数据提供程序。这是你的目标吗?
第一个测试将是退化的情况。给定一个像“name_with_wrong_ending”这样的字符串,你的函数应该失败。应该怎样失败呢?它应该返回 null 吗?或者它应该抛出异常?您需要考虑这一点并在测试中做出决定。然后你就可以通过测试了。
现在,像这样的字符串:“test_file.xml”,但在不存在这样的文件的情况下呢?在这种情况下,您希望该函数执行什么操作?它应该返回 null 吗?它应该抛出异常吗?
当然,测试这一点的最简单方法是在没有该文件的目录中实际运行代码。但是,如果您宁愿编写测试以使其不使用文件系统(明智的选择),那么您需要能够提出问题“此文件是否存在”,然后您的测试需要强制给出答案是“假”的。
您可以通过在类中创建一个名为“isFilePresent”或“doesFileExist”的新方法来做到这一点。您的测试可以覆盖该函数以返回“false”。现在您可以测试当文件不存在时“LoadFile”函数是否正常工作。
当然,现在您必须测试“isFilePresent”的正常实现是否正常工作。为此,您必须使用真正的文件系统。但是,您可以通过创建一个名为 FileSystem 的新类并将“isFilePresent”方法移动到该新类中,将文件系统测试排除在 LocalizationData 测试之外。然后,您的 LocalizationData 测试可以创建该新 FileSystem 类的派生类并覆盖“isFilePresent”以返回 false。
您仍然需要测试 FileSystem 的常规实现,但这是在一组不同的测试中,仅运行一次。
好的,下一个测试是什么?当文件确实存在但不包含有效的 xml 时,您的“loadFile”函数会做什么?它应该做点什么吗?或者这对客户来说是一个问题吗?你决定。但如果您决定检查它,则可以使用与以前相同的策略。创建一个名为 isValidXML 的函数并让测试覆盖它以返回 false。
最后,我们需要编写实际返回 XMLDataProvider 的测试。因此,在所有其他函数之后,“loadData”应该调用的最后一个函数是 createXmlDataProvider。您可以覆盖它以返回空或虚拟的 XmlDataProvider。
请注意,在您的测试中,您从未进入真实的文件系统并真正基于文件创建 XMLDataProvider。但是您所做的就是检查 loadData 函数中的每个 if 语句。您已经测试了 loadData 函数。
现在您应该再编写一个测试。使用真实文件系统和真实有效的 XML 文件的测试。
The problem here is that you are not doing TDD. You wrote the production code first, and now you want to test it.
Erase all that code and start again. Write a test first, and then write the code that passes that test. Then write the next test, etc.
What is your goal? Given a string that ends in "xml" (why not ".xml"?) you want an XML data provider based upon a file whose name is that string. Is that your goal?
The first tests would be the degenerate case. Given a string like "name_with_wrong_ending" your function should fail. How should it fail? Should it return null? Or should it throw an exception? You get to think about this and decide in your test. Then you make the test pass.
Now, what about a string like this: "test_file.xml" but in the case where no such file exists? What do you want the function to do in that case? Should it return null? Should it throw an exception?
The simplest way to test this, of course, is to actually run the code in a directory that does not have that file in it. However, if you'd rather write the test so that it does not use the file system (a wise choice) then you need to be able to ask the question "Does this file exist", and then your test needs to force the answer to be "false".
You can do that by creating a new method in your class named "isFilePresent" or "doesFileExist". Your test can override that function to return 'false'. And now you can test that your 'LoadFile' function works correctly when the file doesn't exist.
Of course now you'll have to test that the normal implementation of "isFilePresent" works correctly. And for that you'll have to use the real file system. However, you can keep file system tests out of your LocalizationData tests by creating a new class named FileSystem and moving your 'isFilePresent' method into that new class. Then your LocalizationData test can create a derivative of that new FileSystem class and override 'isFilePresent' to return false.
You still have to test the regular implementation of FileSystem, but that's in a different set of tests, that only get run once.
OK, what's the next test? What does your 'loadFile' function do when the file does exist, but does not contain valid xml? Should it do anything? Or is that a problem for the client? You decide. But if you decide to check it, you can use the same strategy as before. Make a function named isValidXML and have the test override it to return false.
Finally we need to write the test that actually returns the XMLDataProvider. So the final function that 'loadData' should call, after all those other function is, createXmlDataProvider. And you can override that to return an empty or dummy XmlDataProvider.
Notice that in your tests you have never gone to the real file system and really created an XMLDataProvider based on a file. But what you have done is to check every if statement in your loadData function. You've tested the loadData function.
Now you should write one more test. A test that uses the real file system and a real valid XML file.
当我查看以下代码时:
无论如何,我会问自己以下问题:
IsXML 函数非常简单。甚至可能不属于这个阶层。
如果 LoadFile 函数获取有效的 XML 文件名,则会创建同步 XmlDataProvide。
我首先搜索谁使用 LoadFile 以及 fileName 是从哪里传递的。如果它在我们的程序外部,那么我们需要一些验证。如果它的内部和其他地方我们已经在进行验证,那么我们就可以开始了。正如 Martin 所建议的,我建议重构它以将 Uri 作为参数而不是字符串。
一旦我们解决了这个问题,那么我们需要知道的是 XMLDataProvider 处于同步模式是否有任何特殊原因。
现在,有什么值得测试的吗? XMLDataProvider 不是我们构建的类,我们希望当我们提供有效的 Uri 时它能够正常工作。
坦率地说,我不会为此编写测试浪费时间。将来,如果我们看到更多的逻辑出现,我们可能会再次重新审视这一点。
When I look at the following code:
Anyway, I would ask the following questions to myself:
The IsXML function is extremely trivial. Probably does not even belong to this class.
The LoadFile function creates a synchronous XmlDataProvide if it gets a valid XML filename.
I would first search who uses LoadFile and from where fileName is being passed. If its external to our program, then we need some validation. If its internal and somewhere else we are already doing the validation, then we are good to go. As Martin suggested, I would recommend refactoring this to take Uri as the parameter instead of a string.
Once we address that, then all we need to know is if there is any special reason why the XMLDataProvider is in the synchronous mode.
Now, is there anything worth testing? XMLDataProvider is not a class we built, we expect it to work fine when we give a valid Uri.
So frankly, I would not waste my time writing test for this. In the future, if we see more logic creeping in, we might revisit this again.
在我的一个(Python)项目中,我假设所有单元测试都在包含文件夹“data”(输入文件)和“output”(输出文件)的特殊目录中运行。我正在使用一个测试脚本,它首先检查这些文件夹是否存在(即当前工作目录是否正确),然后运行测试。然后,我的单元测试可以使用相对文件名,例如“data/test-input.txt”。
我不知道如何在 C# 中执行此操作,但也许您可以在测试
SetUp
方法中测试文件“data/azeri.xml”是否存在。In one of my (Python) projects, I assume that all unit tests are run in a special directory that contains the folders "data" (input files) and "output" (output files). I'm using a test script that first checks whether those folders exists (i.e. if the current working directory is correct) and then runs the tests. My unit tests can then use relative filenames like "data/test-input.txt".
I don't know how to do this in C#, but maybe you can test for existence of the file "data/azeri.xml" in the test
SetUp
method.它与您的测试 (x) 无关,但请考虑使用
Uri
而不是String
作为 API 的参数类型。http://msdn.microsoft.com /en-us/library/system.uri(v=VS.100).aspx
x:我认为 Steven 很好地涵盖了这个主题。
It has nothing to do with your testing (x), but consider using
Uri
instead ofString
as parameter type for your API.http://msdn.microsoft.com/en-us/library/system.uri(v=VS.100).aspx
x: I think Steven covered that topic pretty very well.
为什么使用 XmlDataProvider?我不认为这是一个有价值的单元测试,就目前而言。相反,为什么不测试您将使用该数据提供程序执行的操作呢?
例如,如果您使用 XML 数据加载
Foo
对象列表,请创建一个接口:然后您可以使用在单元测试期间生成的测试文件来测试此类的实现。通过这种方式,您可以打破对文件系统的依赖。当您的测试退出时(在finally块中)删除该文件。
而对于使用这种类型的协作者,你可以传入一个模拟版本。您可以手动编写模拟代码,也可以使用模拟框架,例如 Moq、Rhino、TypeMock 或 NMock。模拟很棒,但如果您是 TDD 新手,那么在了解模拟的用途时手工编写模拟代码也是很好的选择。一旦你掌握了这一点,那么你就可以很好地理解模拟框架的好、坏和丑陋。当您开始 TDD 时,使用它们可能会有点困难。您的里程可能会有所不同。
祝你好运。
Why do you use the XmlDataProvider? I don't think that it's a valuable unit test, as it stands now. Instead, why don't you test whatever you would do with that data provider?
For example, if you use the XML data to load out a list of
Foo
objects, make an interface:You can then test your implementation of this class using a test file you generate during a unit test. In this way you can break your dependency on the filesystem. Delete the file when your test exits (in a finally block).
And as for collaborators that use this type, you can pass in a mock version. You can either hand code the mock, or use a mocking framework such as Moq, Rhino, TypeMock or NMock. Mocking is great, but if you're new to TDD then it's fine to hand code your mocks while you learn what they're useful for. Once you have that, then you are in a good position to understand the good, bad and ugly of mocking frameworks. They can be a bit gnarly to work with when you're starting TDD. Your mileage may vary.
Best of luck.
在这种情况下,你基本上处于较低的依赖程度。您正在测试文件是否存在以及是否可以使用该文件作为源来创建 xmlprovider。
打破依赖关系的唯一方法是注入一些东西来创建
XmlDataProvider
。然后,您可以模拟它以返回您创建的 XmlDataProvider(而不是读取)。简单的例子是:使用注入框架可以通过在类构造函数或其他地方注入工厂来简化对 LoadFile 的调用。
In this case, you are basically at the lower level of dependency. You are testing that a file exist and that an xmlprovider can be created with the file as source.
The only way that you could break the dependency, would be to inject something to create the
XmlDataProvider
. You could then mock it to return aXmlDataProvider
that you created (as opposed to read). As simplistic example would be:Using an injection framework could simplify the call to
LoadFile
by injecting the factory in the class constructor or elsewhere.我喜欢@Steven的回答,但我认为他走得还不够远:
我说的不够远是指他没有遵循
最后可能的责任时刻
。尽可能多地推送到已实现的 DataProvider 类中。我没有对这段代码做的一件事是通过单元测试和模拟来驱动它。这就是为什么您仍在检查提供程序的状态以查看其是否有效。
另一件事是我试图消除对让 LocalizationData 知道提供程序正在使用文件的依赖关系。如果它是 Web 服务或数据库怎么办?
I Like @Steven's answer except I think He didn't go far enough:
By not going far enough I mean that he didn't follow the
Last Possible Responsible Moment
. Push as much down into the implementedDataProvider
class as possible.One thing I didn't do with this code, is drive it with unit tests and mocks. That is why you're still checking the state of the provider to see if it is valid.
Another thing is that I tried to remove the dependencies on having the LocalizationData know that the provider is using a file. What if it was a web service or database?
首先让我们了解我们需要测试什么。我们需要验证给定有效的文件名,您的 LoadFile(fn) 方法是否返回 XmlDataProvider,否则返回 null。
为什么 LoadFile() 方法很难测试?因为它创建一个带有从文件名创建的 URI 的 XmlDataProvider。我没有太多使用 C#,但我假设如果系统上实际上不存在该文件,我们将得到一个异常。 真正的问题是,您的生产方法 LoadFile() 正在创建一些难以伪造的东西。无法伪造它是一个问题,因为我们无法确保某个文件在所有测试环境中都存在,而不必强制执行隐式准则。
所以解决方案是 - 我们应该能够伪造 loadFile 方法的协作者 (XmlDataProvider)。然而,如果一个方法创建了它的协作者,它就不能伪造它们,因此方法永远不应该创建它的协作者。
如果一个方法没有创建它的协作者,它如何获得它们? - 通过以下两种方式之一:
在这种情况下,将 XmlDataProvider 注入到方法中是没有意义的,因为这正是它返回的内容。所以我们应该从全局工厂 - XmlDataProviderFactory 获取它。
有趣的部分来了。当您的代码在生产环境中运行时,工厂应返回一个 XmlDataProvider,而当您的代码在测试环境中运行时,工厂应返回一个假对象。
现在唯一的难题是,如何确保工厂在不同的环境下以不同的方式表现?一种方法是使用一些在两个环境中具有不同值的属性,另一种方法是配置工厂应返回的内容。我个人更喜欢前一种方式。
希望这有帮助。
So first of all let us understand what we need to test. We need to verify that given a valid filename, your LoadFile(fn) method returns an XmlDataProvider, otherwise it returns null.
Why is the LoadFile() method difficult to test ? Because it creates a XmlDataProvider with a URI created from the filename. I have not worked much with C#, but am assuming that if the file does not actually exist on the system, we will get an Exception. The real problem is, your production method LoadFile() is creating something which is difficult to fake. Not being able to fake it is a problem because we cannot ensure the existence of a certain file in all test environments, without having to enforce implicit guidelines.
So the solution is - we should be able to fake the collaborators (XmlDataProvider) of the loadFile method. However, if a method creates it's collaborators it cannot fake them, hence a method should never create it's collaborators.
If a method does not create it's collaborators, how does it get them ? - In one of these two ways:
In this case it does not make sense for the XmlDataProvider to be injected into the method, since that is exactly what it is returning. So we should get it from a global Factory - XmlDataProviderFactory.
Here comes the interesting part. When your code is running in production, the factory should return an XmlDataProvider, and when your code is running in a test environment, the factory should return a fake object.
Now the only part of the puzzle is, how to ensure that the factory behaves in different ways in different environments ? One way is to use some properties which have different values in both environments, and the other way is to configure the factory for what it should return. I personally prefer the former way.
Hope this helps.
这次,不要尝试打破对文件系统的依赖。此行为显然取决于文件系统,并且似乎处于与文件系统的集成点,因此请使用文件系统对其进行测试。
现在,我同意鲍勃的建议:扔掉这段代码并尝试对其进行测试。这是很好的练习,也正是我训练自己做到这一点的方式。祝你好运。
This time, don't try to break your dependency on the file system. This behavior clearly depends on the file system, and appears to be at the integration point with the file system, so test it with the file system.
Now, I second Bob's advice: throw this code away and try test-driving it. It makes for great practice and is exactly how I trained myself to do it. Good luck.
不要返回与特定技术相关的 XmlDataProvider,而是隐藏此实现细节。看起来您需要一个存储库角色来
LocalizationData GetLocalizationData(params)
您可以拥有此角色的实现,它在内部使用 Xml。您需要编写集成测试来测试 XmlLocalizationDataRepository 是否可以读取实际的 Xml 数据存储。 (慢的)。
Instead of returning XmlDataProvider which ties you a specific tech, hide this implementation detail. It looks like you need a repository Role to
LocalizationData GetLocalizationData(params)
You can have an implementation for this Role, which internally uses Xml. You'd need to write integration tests to test whether XmlLocalizationDataRepository can read actual Xml data stores. (Slow).