单元测试,集成测试还是功能测试?

6
一个简单的问题:如何区分功能测试、单元测试和集成测试? 有很多不同的观点,但我特别想确定如何组织一个涉及模型关系的 Laravel 测试。这里有一个需要测试的 PHP 代码示例:
public function prices()
{
    return $this->hasMany(Prices::class);
}


public function getPriceAttribute($)
{
    return $this->prices()->first() * 2;
}

据我所理解的测试描述(请随时纠正我): 单元测试 - 测试代码的最小部分 - 不涉及数据库 - 不与系统的任何其他部分交互 集成测试 - 测试系统的部分协同工作 - 例如调用需要一起测试的帮助程序的控制器 功能测试 - 黑盒测试 - 例如调用 API 端点,查看是否返回了正确的 JSON 响应
根据这些描述,我的问题是:
- 我的 Laravel 模型测试需要测试代码的最小单元——计算访问器的模型,这使其感觉像一个单元测试。 - 但是,当它加载模型的关系时,它会触及到数据库。 - 它不像是一个集成测试,因为它只涉及其他相关模型,而不涉及内部或外部服务。 - Laravel 中的其他属性访问器测试在不涉及数据库或模型关系的情况下属于单元测试。 - 将这些类型的测试分成集成测试意味着单个模型针对其属性的测试在集成和单元测试之间被分割。 - 如果不使用模型之间的模拟,那么我的测试应该放在哪里?
答案是:这个测试应该被归类为“集成测试”,因为它与数据库发生了交互。单元测试的目的是测试代码的最小部分,而在这种情况下,模型与数据库交互,因此它不适合作为单元测试。

一个不同的视角(与您的定义非常相似)https://testing.googleblog.com/2010/12/test-sizes.html - dm03514
2个回答

2
如果我正确解释了您最初的问题,我认为此处的关键限制是:
因此,在不嘲讽模型之间的关系的情况下,我的测试应该放在哪里?
如果不允许使用mock并且需要触及数据库,那么根据您和Google的定义,它必须属于集成/中等规模的测试 :)
我认为这样做的方式是,价格属性功能与DB分开。即使它在模型中,但价格可以来自任何地方。现在它是一个RDBMS,但是如果您的组织变得非常庞大并分裂成另一个服务,那怎么办?基本上,我相信getPriceAttributes的功能与属性存储是不同的:
public function getPriceAttribute($)
{
    return $this->prices()->first() * 2;
}

如果您采纳这种推理,它会创建一个逻辑分离,支持单元测试。可以将prices()模拟为返回0个、1个和多个(2个)结果的集合。此测试可作为单元测试执行,以实现数量级更快的测试执行(即按照1毫秒的顺序执行,而不是可能需要与本地数据库通信的10秒或100秒)。
我不熟悉php测试生态系统,但其中一种方法可以使用测试特定子类(不确定以下内容是否有效PHP :p):
class PricedModel extends YourModel {
   function __construct($stub_prices_supporting_first) {
     $this->stub_prices = $stub_prices_supporting_first;
   }

   public function prices() {
     return $this->stub_prices;
   }

}

tests

function test_priced_model_0_prices() {
   p = new PricedModel(new Prices(array()));
   assert.equal(null, p.getPriceAttribute());
}

function test_priced_model_1_price() {
   p = new PricedModel(new Prices(array(1)));
   assert.equal(2, p.getPriceAttribute());
}

function test_priced_model_2_prices() {
   p = new PricedModel(new Prices(array(5, 1)));
   assert.equal(10, p.getPriceAttribute());
}

最初的回答:
希望以上内容能够使您完全控制输入到 getPriceAttribute 方法中,以支持直接无 I/O 单元测试。
此外,以上所有单元测试只能告诉您能否正确处理价格,不能提供有关您是否能查询价格的任何反馈!

1
感谢您详细的回答。我不确定我能否将您回答的第二部分翻译成适用于PHP的可行内容,但我猜您在谈论模拟和存根。我在其他地方使用过它们,但似乎总是存在某种中间测试类型(就像我问题中的那个)。模拟它将非常困难,因为Laravel正在使用该方法作为魔术访问器。我认为您关于它是集成测试的说法是正确的。 - RonnyKnoxville
1
将“getPriceAttribute”移动到其自己的独立函数中,该函数接受价格返回值怎么样?这样直接调用它应该很容易。 - dm03514

1
区分这些测试的关键在于它们各自的目标:
- 单元测试旨在发现软件中可以在孤立的小部分中找到的错误。 (请注意,这并不意味着您必须进行隔离 - 它只是意味着您的重点在于孤立的代码。通常不需要隔离和模拟即可达到此目标:考虑对sin函数的调用 - 您几乎从不需要模拟它,您只需让您的系统在测试时调用原始函数。) - 集成测试旨在发现两个或多个组件交互中的错误,例如关于接口的相互误解。这些错误无法在孤立的软件中发现:如果您在孤立状态下测试代码,则也会根据您(可能有误的)对其他组件的理解编写测试。 - 您所描述的功能测试的目标是查找迄今为止其他测试无法检测到的进一步错误。其中一个示例可能是,集成了旧版本的功能(该版本当时是正确的,但缺少某些功能)。
结论可能会让人惊讶,但严格意义上来说,在单元测试中进行数据库访问并不是被禁止的。考虑以下情况:您开始编写单元测试并模拟数据库访问。后来,您意识到可以更懒惰,只需使用数据库而无需模拟-但是保留所有测试不变。您的测试未更改,并且将继续像以前一样查找隔离代码中的错误。现在它们可能运行得比以前慢一些,并且设置可能比模拟数据库复杂。但是,测试套件的目标是相同的-无论是否模拟数据库。
这种情况使事情变得简单了一些,因为可能存在仅能使用模拟进行的测试用例:例如,测试数据库以特定方式损坏并且您的代码正确处理此问题的情况。使用真实数据库,这样的测试用例可能几乎不可能设置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接