中间地带是否存在?(单元测试 vs 集成测试)

4
考虑实现仓储模式(或类似模式)。我将尽可能简洁地提供示例/说明:
interface IRepository<T>
{
    void Add(T entity);
}

public class Repository<T> : IRepository<T>
{
    public void Add(T entity)
    {
        // Some logic to add the entity to the repository here.
    }
}

在这个特定的实现中,仓储由一个名为IRepository的接口定义,该接口有一个方法将实体添加到仓储中,因此使得仓储依赖于泛型类型T(同时,仓储必须隐式地依赖于另一种类型TDataAccessLayer,因为抽象是仓储模式的全部意义。然而,这种依赖关系目前并不容易获得)。从我目前的理解来看,我有两个选择:单元测试和集成测试。
在集成测试中,可能会涉及更多的移动部分,但我更愿意最初进行单元测试,以至少验证基线功能。但是,如果没有创建某种“实体”属性(泛型类型T),我看不出任何断言Repository实现的Add()方法是否实际执行任何逻辑的方式。
也许,是否有一种介于单元测试和集成测试之间的中间地带,可以通过反射或其他方式验证已经在测试单元内部达到了特定的执行点?
对于这个特定问题,我想到的唯一解释是进一步将数据访问层从仓储中抽象出来,导致Add()方法不仅接受实体参数,还接受数据访问参数。但是,这似乎会破坏仓库模式的目的,因为仓库的使用者现在必须知道数据访问层。
关于示例请求:
(1)就单元测试而言,我不确定像仓储这样的东西是否可以使用我对当前测试技术的理解进行单元测试。因为仓储是特定数据访问层周围的抽象(包装器),所以似乎唯一的验证方法是集成测试? (当然,仓储接口可能不与任何特定的DAL相关联,但是任何实现的仓储肯定与特定的DAL实现相关联,因此需要能够测试Add()方法是否实际执行某些工作)。
(2)就集成测试而言,据我了解,测试将通过实际调用Add()方法(应该将记录添加到仓库中)并检查数据是否实际添加到仓库(或在特定场景下的数据库中)来验证Add()方法是否执行工作。这可能看起来像:
[TestMethod]
public void Add()
{
    Repository<Int32> repository = new Repository<Int32>();
    Int32 testData = 10;

    repository.Add(testData);

    // Intended to illustrate the point succinctly. Perhaps the repository Get() method would not
    // be called (and a DBCommand unrelated to the repository issued instead). However, assuming the
    // Get() method to have been previously verified, this could work.
    Assert.IsTrue(testData == repository.Get(testData));
}

所以,在这种情况下,假设存储库是数据库逻辑层的包装器,测试期间实际上会两次访问数据库(一次在插入期间,一次在检索期间)。
现在,我能看到有用的技术是验证某个执行路径在运行时是否被采取。例如,如果传递了非空引用,则验证采取执行路径A,如果传递了空引用,则验证采取执行路径B。此外,也许可以验证要执行特定的LINQ查询。因此,在测试期间实际上从未访问数据库(允许原型设计和开发实现而不需要任何实际的数据访问层)。

在这里,你究竟把"单元测试"和"集成测试"之间的界限划在哪里?假设你想对"Add"方法进行单元测试 - 你能举个例子说明一个单元测试应该是什么样子的,一个集成测试应该与之有何不同? - Doc Brown
1
你究竟想要测试什么?一个具体的实现吗?一个可以遵循的思路是:如果你正在使用TDD,那么在没有单元测试的情况下,你将不会有一个具体的实现。因此,询问自己想要实现哪些功能就等同于询问你想要通过哪些测试。 - John Saunders
@John Saunders 啊,好的,现在我们有点头绪了。我会研究一下行为并尝试找到一些关于行为驱动开发的信息。这可能正是我一直在寻找的! - Bradford Fisher
@Brad:这与测试什么会出错形成了鲜明的对比。例如:我刚刚维护了一个每财年会出现一次故障的SP - 需要手动初始化数据库每年一次。因为赶时间,所以没有测试更改。一旦QA发现了错误,我回去写了一个失败的单元测试,然后通过修复错误使其通过。虽然我没有使用mocks,但我在事务下运行了整个过程,并在测试结束时将其回滚。比重构直接的DAL代码以允许模拟要快得多。 - John Saunders
@John Saunders 好的,没问题,我只是想给你提供选择。一旦我手头有一个可行的解决方案,我会回答这个问题,以便其他人也能受益。我想这个问题更多地涉及到要单元测试什么以及如何进行测试,而不是InsertOnSubmit是否会失败的问题。 - Bradford Fisher
显示剩余11条评论
3个回答

2

听起来你描述的是测试实现细节而不是模式实现者满足要求。在测试单元内是否达到了“特定执行点”并不重要,重要的是具体的实现者是否遵守接口的约定。为测试目的创建一个T实体是完全可以接受的,这就是模拟对象的作用。


当然,我可以模拟一个实体对象,但我的问题是在单元测试中我到底应该对什么进行断言呢?调用repository.Add(someMockObject)对我来说并不是很有用,除非我能验证对someMockObject采取了某些操作。 - Bradford Fisher
仓库必须满足的要求是持久化您添加的对象。测试“仅添加”存储库是徒劳的,因为您正在测试一个无用的存储库。测试任意存储库实现中的“添加”方法的唯一方法是要求它返回您添加的对象。 - Aidan Ryan
这基本上是@JohnSaunders所说的相同内容,与其以教条主义的“测试模式”术语思考,不如以“证明实施者执行其所需执行的操作”术语思考。重要的是测试不知道实现细节,而不是它是“集成”还是“单元”。 - Aidan Ryan

0

如果您想进行集成测试,您需要使用真实的数据库。但是,如果您想快速测试某些东西,您可以尝试使用内存数据库。 问题在于您可以测试什么和您无法测试什么。只要您的数据库访问代码是特定于数据库的,您就在使用外部系统(为了保持单元测试),您应该模拟。但由于您真正想知道的是数据是否最终到达数据库,因此您需要针对真实数据库进行测试。

但是,如果您使用一些db抽象化,例如ORM映射器,您可以使用ORM映射器并测试是否至少正确工作了映射。然后,ORM映射器可以为您的测试使用内存数据库,以检查ORM映射器是否按预期工作。

如果您不使用ORM映射器,并且创建一个额外的db抽象层,只是为了有一个错误,您想要在真正的单元测试中揭示这个错误,那么这并不会让您更加高效。


0
不要进行单元测试,只需进行集成测试。实现单元测试非常容易,但在仓储模式中几乎没有用处。几乎所有的错误都发生在直接与数据库和数据逻辑打交道的过程中。单元测试无法找到此类错误。除非您使用复杂计算和许多可能结果的方法,否则应完全避免使用单元测试。另一个好处是使用相同代码进行负载测试。您所询问的中间/临时解决方案将毫无意义。
用户故事是什么?
从数据库获取员工详细信息。
如何完成任务?
编写代码并测试是否从数据库中获取了员工详细信息。记录下您能想到的所有场景。我们只谈论场景(不多不少)。
集成测试或功能测试必须直接处理数据库/源。不使用模拟。以下是步骤。您希望测试getEmployee(emp_id)。以下这5个步骤都在单个测试方法中完成。
  1. 删除数据库
  2. 创建数据库并填充角色和其他基础数据
  3. 使用ID创建员工记录(这不应该使用存储库或实际的代码)。您需要另一个代码将数据放入测试项目中的数据库。种子数据需要一组代码。
  4. 使用此ID调用repository.getEmployee(emp_id)//这是您编写的代码,需要进行测试

现在进行Assert()/验证以确保返回的数据正确。

这证明getEmployee()函数有效。步骤1到4需要您拥有仅由测试项目使用的代码。步骤4调用应用程序代码。我的意思是创建员工(步骤3)应由测试项目代码完成,而不是应用程序代码。如果有用于创建员工的应用程序代码(例如:CreateEmployee()或Repository.Add(Employee)),则不应使用该代码。同样,当我们测试CreateEmployee()函数时,不应使用GetEmployee()应用程序代码。我们应该有一个用于从表中获取数据的测试项目代码。

这样就不需要使用模拟!删除和创建数据库的原因是防止数据库出现损坏的数据。按照我们的方法,无论运行多少次,测试都会通过。

你谈到了场景,这段代码是否执行过或者是否执行了那个条件等等。这需要使用输入数据和输出数据(来自原始来源,无论是数据库还是日志文件)进行验证。
举个例子,如果传入一个非空引用,验证是否执行了路径A,如果传入空引用,则验证是否执行了路径B。
所以你将空引用传递给存储库方法。然后你调用数据库(使用你的测试项目代码)并检查错误表中是否有条目。或者上传日志文件并检查是否添加了错误(再次使用你自己的测试代码或第三方组件)。你可能会觉得在测试项目中编写了很多定制代码。实际上,这些只是一次性的简单的获取和放置操作,而且会一直保留下去。
这篇文章讨论了集成测试相对于单元测试的好处,因为单元测试杀死了!(它说的)。

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