现在,我还读到建议使用模拟框架来避免针对数据库运行测试。为什么会这样?如果我针对测试数据库运行测试,这是否可以接受?
编写单元测试(在您的情况下为 TDD)的全部意义在于,您希望专注于编写最少的代码以通过测试。如果您的类依赖于另一个类或数据库,则该类和数据库不存在。因此,您编写了足够的代码来通过测试并模拟其他所有内容。考虑这个要求:“给定一个字符串,从字符串中取出所有x 字符,然后将结果保存到数据库中”。你要做的第一件事就是写一个测试:
[TestMethod]
public void Extract_WhenCalledWithEmptyString_ShouldReturnEmpty()
{
var extr = new XExtractor();
var extracted = extr.Extract(string.Empty);
Assert.IsTrue(extracted == string.Empty);
}
运行您的测试和编译将失败,因为XExtractor 不存在。所以我们需要通过它。我们继续写课程。
public class XExtractor
{
public string Extract(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
}
}
运行测试,它有望通过。然后你的下一个测试。然后您最后编写一个测试,如果一切正常并且找到字符x(s),XExtractor 必须为特定字符串至少调用一次您的数据访问层类。因此,要通过测试,您需要为数据访问层类编写最少的代码以通过测试,这意味着使用一种方法的简单接口。然后你模拟这个接口并运行你的测试并在模拟上做断言以确保它被调用一次并且它被传递了你期望传递给它的字符串。在整个过程中,您都专注于测试XExtractor 中的代码并确保它正在调用另一个依赖项。在这种依赖关系中会发生什么,此时您并不关心。以下是有帮助的:假设您不会编写数据访问层,但其他人会编写它,例如,我将在加拿大一直编写它。我会确保无论你传递给我什么字符串,我都会调用一些存储过程并将字符串传递给它。我将进行数据库连接等,我将根据您和我同意的某些合同(接口)返回结果。其他人将编写存储过程。然而,一旦我们都完成了,有人会将你的类、我的类和存储过程相互连接起来,它应该就可以工作了。这发生在Composition Root。
完成XExtractor 的测试后,您开始为数据访问层类编写代码。但是现在你需要专注于这个类,并假装你从未写过XExtractor 类。因此,您所拥有的只是该类的公共接口。所以你现在需要为数据访问层编写所有的测试,但是这个类需要数据库。没有数据库,所以你写一个接口,然后模拟接口。
现在您知道您的两个课程都有效。您继续到您的数据库,您可能需要编写一个存储过程。您也编写单元测试以通过 sp。然后你用真实的实现替换所有的模拟,并将所有的对象与真实的对象连接起来。
另外 - 是否可以在控制器级别编写从业务层调用方法的测试,或者我应该将单元测试的覆盖范围集中在一种方法上?但是如果该方法正在调用另一个方法呢?
当然你需要这样做。这就是模拟的用途。在上面的示例中,当您编写XExtractor 时,如果它必须调用数据访问层,那么您将模拟数据访问层。然后针对它运行断言以确保交互发生。
可测试代码是否意味着我必须进行依赖注入
是和不是。是的,因为在完成所有模拟之后,您可以要求 DI 容器将所有真正的依赖项相互注入。你甚至可以有一个配置文件:一个用于测试依赖项,一个用于真正的依赖项。编写一次配置,让容器为您完成所有工作。但是如果你刚刚开始,那么就不要依赖注入。一旦你经历了插入东西和做管道的痛苦,然后转移到依赖注入容器,因为这样你就会看到它的真正价值。
并针对接口调用方法并接受参数作为接口?
您会注意到界面将使您的工作变得更加轻松。许多测试框架可以轻松地模拟接口,这非常好。要模拟类,您需要将方法设为虚拟,否则某些模拟框架将无法模拟它们。但是仅仅因为你使用接口并不意味着你的类不能使用继承和抽象。您仍然可以这样做,但只需确保它们实现所需的接口即可。
还有几点
几个月后,您可能会收到不同的要求,因为另一个客户想要提取所有 y 字符,但他们不想为 x 提取器付费(想象一下,这非常复杂)。在这种情况下,您编写另一个提取器,而我和编写 sp 的其他开发人员不必担心,因为我们只是从您那里获取一个字符串并保存它。因此,您编写课程,运行测试,然后负责将它们连接在一起的人将把它卖给另一个客户。但是这个客户会得到YExtractor。你明白了……
最后确保您了解模拟和存根之间的区别。上面例子中测试XExtractor的时候,需要mock数据访问层类。为什么是模拟的?因为如果你的类不调用数据访问层类,测试就会失败。如果有什么东西可以让你的测试失败,那就是模拟。否则,它就是一个存根,它只是用来支持您的测试。