您是否应该在所有单元测试中对数据进行硬编码?


33

那里的大多数单元测试教程/示例通常涉及为每个单独的测试定义要测试的数据。我猜这是“一切都应该隔离测试”理论的一部分。

但是我发现,当处理具有大量DI的多层应用程序时,设置每个测试所需的代码将花费很长的时间。取而代之,我建立了许多测试库类,现在可以继承这些类,其中已经预先构建了许多测试框架。

作为此过程的一部分,我还将构建代表运行中的应用程序数据库的伪数据集,尽管每个“表”中通常只有一两行。

是否预先定义(如果不是全部)所有单元测试中的大多数测试数据,这是公认的做法吗?

更新资料

从下面的评论中,确实感觉到我在进行比单元测试更多的集成。

我当前的项目是ASP.NET MVC,它使用实体框架代码优先的工作单元和Moq进行测试。我已经模拟了UoW和存储库,但是我正在使用真实的业务逻辑类,并测试控制器操作。测试通常会检查UoW是否已提交,例如:

[TestClass]
public class SetupControllerTests : SetupControllerTestBase {
  [TestMethod]
  public void UserInvite_ExistingUser_DoesntInsertNewUser() {
    // Arrange
    var model = new Mandy.App.Models.Setup.UserInvite() {
      Email = userData.First().Email
    };

    // Act
    setupController.UserInvite(model);

    // Assert
    mockUserSet.Verify(m => m.Add(It.IsAny<UserProfile>()), Times.Never);
    mockUnitOfWork.Verify(m => m.Commit(), Times.Once);
  }
}

SetupControllerTestBase正在构建模拟单元,并实例化userLogic

许多测试需要在数据库中拥有现有用户或产品,因此在此示例中userData,我已经预先填充了模拟UoW返回的内容,它只是一个IList<User>用户记录。


4
教程/示例的问题在于它们必须很简单,但是您无法在简单的示例中显示解决复杂问题的方法。它们应附有“案例研究”,以描述该工具如何在合理规模的实际项目中使用,但很少使用。
2013年

也许您可以添加一些自己不满意的小代码示例。
卢克·弗兰肯

如果您需要大量设置代码来运行测试,则可能会冒险运行功能测试。如果在更改代码时测试失败,但是代码没有问题。这绝对是一项功能测试。
Reactgular

“ xUnit测试模式”这本书为可重复使用的固定装置和辅助程序提供了有力的证明。测试代码应与其他任何代码一样可维护。
Chuck Krutsinger 2013年

Answers:


25

最终,您希望编写尽可能少的代码以获得尽可能多的结果。在多个测试中使用大量相同的代码,a)倾向于导致复制粘贴编码,b)意味着,如果方法签名发生更改,您最终将不得不修复许多损坏的测试。

我使用具有标准TestHelper类的方法,这些类为我提供了我经常使用的许多数据类型,因此我可以为测试创建标准实体或DTO类的集合,以查询并确切知道每次将得到什么。因此,我可以调用TestHelper.GetFooRange( 0, 100 )以获取100个Foo对象及其所有相关类/字段集的范围。

特别是在ORM类型系统中配置了复杂关系的情况下,必须存在这些关系才能使事物正确运行,但对于此测试而言并不一定很重要,因为它可以节省大量时间。

在要测试接近数据级别的情况下,有时我会创建存储库类的测试版本,可以以类似的方式查询(同样,这是在ORM类型的环境中,并且与版本无关)真实数据库),因为要模拟出对查询的确切响应是一项繁重的工作,而且通常只会带来次要的好处。

尽管在单元测试中,有些事情要小心:

  • 确保您的模拟模拟。如果要进行单元测试,则围绕被测试类执行操作的类必须是模拟对象。您的DTO /实体类型类可能是真实的东西,但是如果类正在执行操作,则需要对它们进行模拟-否则,当支持代码更改并且测试开始失败时,您必须花很多时间来找出哪种更改实际造成了问题。
  • 确保正在测试您的课程。有时,如果通过一组单元测试进行查看,很明显,有一半的测试实际在测试模拟框架,而不是他们应该测试的实际代码。
  • 不要重用模拟/支持对象这是一个大问题-当人们开始尝试聪明地使用支持单元测试的代码时,很容易无意间创建在测试之间持久存在的对象,这可能会产生不可预测的结果。例如,昨天我有一个测试,该测试在单独运行时通过,在类中的所有测试都运行时通过,但是在整个测试套件运行时失败。事实证明,在测试助手中存在一个偷偷摸摸的静态对象,当我创建它时,绝对不会造成问题。只需记住:在测试开始时,所有内容都会创建,在测试结束时,所有内容都将被销毁。

10

任何使您的测试意图更具可读性的方法。

作为一般经验法则:

如果数据是测试的一部分(例如,不应打印状态为7的行),则在测试中对其进行编码,这样可以清楚作者的意图。

如果数据只是填充数据,以确保要使用某些数据(例如,如果处理服务抛出异常,则不应将记录标记为已完成),则一定要使用BuildDummyData方法或测试类,以将无关的数据排除在测试之外。

但是请注意,我正在努力思考后者的一个很好的例子。如果在单元测试夹具中有许多这样的工具,则可能要解决另一个问题……也许被测方法太复杂了。


我同意+1。闻起来就像他正在测试要紧密耦合以进行单元测试一样。
Reactgular 2013年

5

不同的测试方法

首先定义您正在做什么:单元测试集成测试。层数与单元测试无关,因为您最有可能只测试一个类。剩下的你嘲笑。对于集成测试,不可避免地要测试多个层。如果您有良好的单元测试,则诀窍是使集成测试不太复杂。

如果您的单元测试很好,那么在进行集成测试时不必重复测试所有细节。

我们使用的术语有点依赖于平台,但是您几乎可以在所有测试/开发平台中找到它们:

应用范例

根据您使用的技术,名称可能会有所不同,但是我将以此为例:

如果您有一个简单的CRUD应用程序,其模型为Product,ProductsController和一个索引视图,该视图生成包含产品的HTML表:

该应用程序的最终结果是显示一个HTML表,其中包含所有活动产品的列表。

单元测试

模型

您可以轻松测试的模型。有不同的方法。我们使用固定装置。我认为这就是您所说的“伪数据集”。因此,在运行每个测试之前,我们先创建表,然后放入原始数据。大多数平台都有用于此的方法。例如,在您的测试类中,有一个方法setUp()在每个测试之前运行。

然后,我们运行测试,例如:testGetAllActive产品。

因此,我们直接测试到测试数据库。我们不模拟数据源;我们使它始终相同。例如,这使我们可以测试数据库的新版本,并且会出现任何查询问题。

在现实世界中,您不能总是遵循100%的单一责任。如果您想做得更好,可以使用模拟的数据源。对我们来说(我们使用ORM)就像测试现有技术一样。而且测试变得更加复杂,并且它们实际上并没有测试查询。所以我们保持这种方式。

硬编码数据分别存储在灯具中。因此,固定装置就像一个带有创建表语句的SQL文件,并为我们使用的记录插入。除非确实需要对大量记录进行测试,否则我们将它们保持在很小的范围内。

class ProductModel {
  public function getAllActive() {
    return $this->find('all', array('conditions' => array('active' => 1)));
  }
}

控制者

控制器需要做更多的工作,因为我们不想用它来测试模型。因此,我们要做的是模拟模型。这意味着:我们测试:index()方法,该方法应返回记录列表。

因此,我们模拟了模型方法getAllActive()并在其中添加了固定数据(例如,两条记录)。现在,我们测试控制器发送到视图的数据,并比较是否确实获得了这两个记录。

function testProductIndexLoggedIn() {
  $this->setLoggedIn();
  $this->ProductsController->mock('ProductModel', 'index', function(return array(your records) ));
  $result=$this->ProductsController->index();
  $this->assertEquals(2, count($result['products']));
}

够了 我们尝试向控制器添加尽可能少的功能,因为这会使测试变得困难。但是,当然总会有一些代码。例如,我们测试需求,例如:仅在登录时才显示这两个记录。

因此,控制器通常需要一个模拟文件和一小部分硬编码数据。对于登录系统,可能是另一个。在我们的测试中,我们有一个辅助方法:setLoggedIn()。这使登录或不登录的测试变得简单。

class ProductsController {
  public function index() {
    if($this->loggedIn()) {
      $this->set('products', $this->ProductModel->getAllActive());
    }
  }
}

观看次数

视图测试很难。首先,我们分离出重复的逻辑。我们将其放在Helpers中,并严格测试这些类。我们期望总是相同的输出。例如,generateHtmlTableFromArray()。

然后,我们有一些项目特定的视图。我们不测试那些。真正不需要对它们进行单元测试。我们保留它们进行集成测试。由于我们将大量代码提取到视图中,因此此处的风险较低。

如果您开始进行测试,则每次更改一段HTML对大多数项目都没有用时,您可能需要更改测试。

echo $this->tableHelper->generateHtmlTableFromArray($products);

整合测试

根据这里的平台,您可以处理用户故事等。它可以基于网络,例如Selenium或其他类似的解决方案。

通常,我们只是使用固定装置加载数据库,并断言哪些数据应该可用。对于全面集成测试,我们通常使用非常全局的要求。因此:将产品设置为活动状态,然后检查该产品是否可用。

我们不会再次测试所有内容,例如是否提供正确的字段。我们在这里测试更大的要求。由于我们不想从控制器或视图复制测试。如果某些内容确实是您应用程序的关键/核心部分,或者出于安全原因(请检查密码不可用),则我们添加它们以确保它是正确的。

硬编码数据存储在灯具中。

function testIntegrationProductIndexLoggedIn() {
  $this->setLoggedIn();
  $result=$this->request('products/index');

  $expected='<table';
  $this->assertContains($expected, $result);

  // Some content from the fixture record
  $expected='<td>Product 1 name</td>';
  $this->assertContains($expected, $result);
}

对于一个完全不同的问题,这是一个很好的答案。
pdr

感谢您的反馈。您可能是对的,我没有提到太具体。之所以给出冗长的答案,是因为在测试所提出的问题时,我发现最困难的事情之一。隔离测试如何适合各种测试的概述。这就是为什么我在每个部分中都添加了如何处理(或分离)数据的原因。请看一下是否可以弄清楚。
吕克·弗兰肯

答案已通过一些代码示例进行了更新,以说明如何在不调用各种其他类的情况下进行测试。
卢克·弗兰肯

4

如果要编写涉及大量DI和接线的测试,直到使用“真实”数据源,您就可能离开了普通单元测试领域,而进入了集成测试领域。

我认为,对于集成测试,拥有通用的数据设置逻辑并不是一个坏主意。此类测试的主要目标是证明所有配置均正确。这完全独立于通过系统发送的具体数据。

另一方面,对于单元测试,我建议将测试类的目标保持为单个“真实”类,并嘲笑其他所有内容。然后,您应该对测试数据进行硬编码,以确保覆盖了尽可能多的特殊/先前的错误路径。

为了向测试添加半硬编码/随机元素,我喜欢引入随机模型工厂。在使用模型实例的测试中,我随后使用这些工厂创建有效但完全随机的模型对象,然后仅对即将进行的测试感兴趣的属性进行硬编码。这样,您可以直接在测试中指定所有相关数据,同时省去了同时指定所有不相关数据和(在一定程度上)测试是否对其他模型字段没有意外依赖的需求。


-1

我认为对大多数数据进行硬编码以进行测试非常普遍。

考虑一个简单的情况,其中特定的数据集导致错误发生。您可能专门为该数据创建了一个单元测试,以执行修复程序并确保该错误不会再次出现。随着时间的流逝,您的测试将具有一组涵盖许多测试用例的数据。

预定义的测试数据还允许您构建一组涵盖广泛且已知情况的数据。

就是说,我认为在测试中包含一些随机数据也很有价值。


您真的阅读了问题,而不仅仅是标题吗?
2013年

在测试中拥有一些随机数据的价值 -是的,因为没有什么比尝试找出每周一次失败的测试中发生的事情更像了。
pdr

在测试中使用随机数据进行混浊/模糊测试/输入测试很有价值。但这不是您的单元测试,那将是一场噩梦。
glenatron 2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.