使用 C# 与 Neo4j 进行集成测试

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

与旨在专注于隔离代码的小单元的原型单元测试不同,集成测试是一种测试类型,通常旨在测试软件系统的两个或多个互连部分之间的交互。

集成测试产生高回报的一个常见领域是应用程序和数据库后端之间的交互点。这种类型的集成测试允许验证向数据库发出的查询的预期行为,以及随后将该数据集转换为域模型或代码中的其他数据结构。

虽然 NoSQL C# 项目越来越普遍,但大多数企业应用程序仍然使用传统 SQL 数据库对数据建模。因此,很难找到使用 NoSQL 解决方案构建集成测试 1 的良好示例和指南。

然而,无论您使用的是 SQL 还是 NoSQL 数据库 ,编写可重复的集成测试几乎总是依赖于利用数据库事务,因为这允许在集成测试期间创建的数据被视为临时数据,在测试用例完成时自动清理.

当然,几乎所有应用程序都必须具有事务功能,但它对开发人员在创建集成测试时特别有用。

Neo4j 和 Neo4jClient

从 2.0 开始, Neo4j 引入了事务性 HTTP 端点,允许基于非 Java 的编程语言绑定利用完全事务性图形数据库。

对于 C# 环境,开发人员通常可以使用两个主要绑定: Neo4jClient Cypher.NET 。后者是在考虑交易的情况下构建的;然而,就个人品味而言,我更喜欢 Neo4jClient 的流畅功能来编写 Cypher 查询。

此外,当我不得不在 2014 年初决定在我公司的企业规模项目中使用哪个库时,Neo4jClient 似乎是这两个项目中更活跃的项目,但该库没有事务实现。

我有两个选择:要么选择 Cypher.NET,要么利用 Neo4jClient 的开源模型并自行实现事务功能。我选择了后者,它帮助我和我的团队轻松编写和理解 Cypher 查询,同时在编写它们时知道我们可以为我们的模块编写集成测试并轻松回归测试对查询或数据处理所做的任何更改.

从 Neo4jClient 1.1.0.x 开始,您可以将事务实现用于您自己的项目,无论是用于测试还是在解决方案 2 的建模中。

Neo4jClient 事务实现引入了反映事务 API 的新接口。这些如下 3


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

启动和管理事务非常简单,并且在典型的 SQL 客户端事务中使用许多相同的约定:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

Neo4j 的事务机制现在不会立即创建节点,而是等到提交已发出,以使此类数据可供所有读者使用。除了这个方面,我们还将探讨这段代码中的两个细节。

第一个是 ITransaction 实现 IDisposable ,即当代码流退出 using 块时,事务必须已经提交,否则它会自动回滚(与典型 SQL 客户端事务会遇到的相同模式)。

例如:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

第二个细节是 graphClient 对象知道在调用 ExecuteWithoutResults() 时它是在事务内部。这是通过使用 Microsoft 的 System.Transactions 使用的相同机制来实现的,即环境事务或存储在线程级别的事务对象(使用 ThreadStaticAttribute )。这意味着,虽然没有对 ITransactionalGraphClient.EndTransaction() ITransaction.Close() 的显式或隐式调用,但在同一线程中通过 graphClient 执行的所有 Cypher 查询都将是同一事务的一部分。

使用 BeginTransaction(TransactionScopeOption) 可以通过三种方式启动“事务块”:

    • TransactionScopeOption.Join :这是使用 BeginTransaction() 时的默认值。它指示客户端加入现有的环境事务(如果存在),否则启动新事务。
    • TransactionScopeOption.RequiresNew :始终启动新事务,即使已经存在另一个环境事务。
    • TransactionScopeOption.Suppress :表示以下查询不会成为环境事务的一部分(如果有的话)。

需要注意的是,即使 TransactionScopeOption.Join 指示客户端加入环境事务,它仍然必须被视为事务块,即在显式或隐式调用之前必须调用 Commit() ITransaction.Close()

例如:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

此外,当调用 nestedTransaction.Commit() 时,并没有实际提交到 Neo4j,而只是将嵌套事务块标记为“成功”。提交发生在父调用 Commit() 时。通过使用前面描述的机制,编写集成测试是干净的,它不会干扰以前编写的代码,即使您的程序确实使用事务(除非它使用 TransactionScopeOption.RequiresNew )。

假设您有 Entity ;和 EntityRepository 类:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

前面的代码允许 EntityRepository 实例使用事务 4 Entity 对象存储到 Neo4j 中。现在假设您要测试 Add() 方法并确保对象已成功存储到 Neo4j 中。一种方法是进行以下测试:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

这很好,但是多次运行后,您最终会得到一个充满测试节点的图表。有没有办法不创建节点并仍然使用 Neo4j 进行集成测试?这是交易派上用场进行测试的地方:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

通过使用夹具级存储库,我们可以确保正在使用的 GraphClient 实例是保持事务打开的实例,并且通过使用 SetUp 和 TearDown 方法,我们可以启动和回滚事务。

请注意,默认情况下,在 Add() 方法内部使用的事务将加入已在 SetupTransactionContext() 内部创建的环境事务,即使它调用了提交,“父”事务也没有。因此,任何创建或更改的数据都将回滚,数据库将返回到原始状态。

事务客户端的实现是在与 Microsoft 的 System.Transactions 集成的情况下进行的。这允许系统将 SQL 事务和 Neo4j 事务结合起来,甚至可以将它们一起用于测试:


 public interface ITransactionGraphClient : IGraphClient
{
    ITransaction BeginTransaction();
    ITransaction BeginTransaction(TransactionScopeOption scopeOption);
    void EndTransaction();
}

public enum TransactionScopeOption { Join, // default value RequiresNew, Suppress }

public interface ITransaction : IDisposable { void Commit(); void Rollback(); }

缺点

并非一切都是完美的,我发现了一些可能比 showstoppers 更烦人的问题:

    • 您需要一个“全局” GraphClient 实例。这通常是个不错的主意(因为 Connect() 调用非常昂贵),并且可以通过 Dependency Injection 轻松处理,但是,如果您的代码在您的项目中创建此实体的实例,则使用本文中描述的技术文章可能涉及更改您的代码库。
    • 当您回滚创建的节点或关系时,ID 不会被回收(或至少暂时不会),这意味着 Neo4j 用来跟踪已用 ID 的文件不断增长,尽管它们充满了未使用的 ID,并且一段时间后(以 数百万次 运行的规模),图形数据库的性能会下降,因此您的测试运行将花费更长的时间。解决方案非常简单:使用一个特殊的数据库进行测试,当性能变得非常糟糕时,您可以将其恢复(或删除并重新创建)到之前的某个点。

结论

感谢 Totham Oddie 和他在 Readify 的团队的所有出色工作,将事务集成到 Neo4jClient 库中现在允许开发人员继续使用 Cypher 的流畅 API 的简单性,并在 .网络社区。您通过授权集成测试阅读了本文中的一个示例;但它也让可能投资于使用 Neo4j 的产品的公司放心,方法是了解来自开源社区的支持。

在此处 查找用于集成测试示例的代码。

参考

1 与单元测试相反,集成测试通常包括程序或模型使用的存储机制。

2 NuGet 中的最新版本已包含事务实现。

3 接口声明中还有其他方法和属性。但是,为了简单起见,我只展示了我在本文中解释的内容。

4 这里使用一个事务来举例说明用法,尽管对于这样一个简单的场景来说显然不需要。

最初由 InnovoCommerce.com 高级软件工程师 Arturo Sevilla 撰写,并发布在 Neo4j 博客上。