这个开源测试框架如何随着 .NET 发展

重新评估和彻底改革软件项目的设计是跟上环境变化的关键步骤。
53 位读者喜欢这篇文章。
magnifying glass on computer screen, finding a bug in the code

Opensource.com

软件项目的设计是编写时的时代背景的产物。 随着环境的变化,明智的做法是退后一步,思考旧的想法是否仍然适合良好的设计。 如果不是,您可能会错过增强功能、简化、新的自由度,甚至项目本身的生存。

对于 .NET 开发人员来说,这是一个相关的建议,他们的依赖项不断更新或正在为 .NET 5 做准备。Fixie 项目在 .NET Core 的早期采用阶段适应外部环境时,就面临着这种现实。 Fixie 是一个开源 .NET 测试框架,类似于 NUnit 和 xUnit,侧重于 开发者人体工程学 和自定义。 它在 .NET Core 之前开发,并经历了几个主要的设计改革,以响应平台更新。

问题:可靠的程序集加载

.NET 测试项目往往感觉很像一个库:一堆没有可见入口点的类。 假设测试运行程序(例如 Fixie 的 Visual Studio Test Explorer 插件)将加载您的测试程序集,使用反射来查找其中的所有测试,并调用测试以收集结果。 与常规库不同,测试项目与常规控制台应用程序有一些相似之处

  1. 与任何可执行文件一样,测试项目的依赖项应该可以从它们自己的构建输出文件夹中自然加载。
  2. 当运行多个测试项目时,测试项目 A 的加载程序集应与测试项目 B 的加载程序集分开。
  3. 当被测系统依赖于 App.config 文件时,它应该在测试运行时使用测试项目本地的文件。

我将这些行为称为“三大要素”。 “三大要素”是如此自然,以至于您很少需要出来。 测试项目应该类似于控制台可执行文件:它应该能够拥有依赖项,它不应与为另一个项目加载的程序集冲突,并且每个项目都应尊重其自己专用的配置文件。 我们认为这一切都是理所当然的。 天是蓝色的,水是湿的,“三大要素”必须在测试运行时得到尊重。

Fixie v1:为“三大要素”设计

“三大要素”为 .NET 测试框架带来了巨大的问题:主运行进程(例如 Visual Studio Test Explorer)根本不在测试项目的构建输出文件夹附近。 加载测试项目并运行它的最自然尝试将使所有“三大要素”都失败。

Fixie 的早期 alpha 版本对程序集加载很幼稚:测试运行程序 .exe 将加载一个测试项目并运行简单的测试,但是当测试尝试运行另一个程序集中的代码(例如被测试的应用程序)时,它将失败。 默认情况下,它会在测试运行程序附近搜索程序集,而不在测试项目的构建输出文件夹附近。

一旦我们解决了这个问题,使用该测试运行程序运行多个测试项目将导致运行时冲突,例如当每个测试项目引用同一库的不同版本时。

当我们解决这个问题后,测试运行程序将无法在正确的配置文件中查找,错误地认为测试运行程序的配置文件是要使用的配置文件。

在旧的 .NET Framework 时代,解决“三大要素”的方法以 AppDomain 的形式出现。 AppDomain 是一项相当古老且现在已弃用的技术。 Fixie v1 是在 AppDomain 是主要解决方案时开发的,并且在当时没有弃用的迹象,可以解决“三大要素”。 在这些情况下,使用 AppDomain 解决“三大要素”是理想的设计,尽管使用它们有点令人沮丧。 简而言之,它们允许单个测试运行程序划分出加载程序集的小口袋,并在它们之间建立严格的通信边界。

Fixie version 1

Test Explorer 插件及其自身的依赖项(例如 Mono.Cecil)位于一个 AppDomain 中。 测试程序集及其依赖项位于第二个 AppDomain 中。 痛苦的序列化边界允许请求跨越鸿沟,而不会有混合加载程序集的风险。

AppDomain 允许您将每个测试项目的构建输出文件夹标识为该测试项目的配置文件和依赖项的所在地。 您可以成功地将测试项目的文件夹加载到测试运行程序进程中,调用它,并在满足“三大要素”要求的同时获得测试结果。

然后 .NET Core 出现了。 突然之间,AppDomain 成为了一个古老且已弃用的概念,它根本不会继续存在于 .NET Core 世界中。

情况发生了翻天覆地的变化。

Fixie v2:适应 .NET Core 危机

起初,这似乎是 Fixie 项目的终结。 整个设计都依赖于 AppDomain,如果这个新奇的 .NET Core 东西幸存下来,Fixie 将无法解决“三大要素”。 绝望。 关门大吉。 删除存储库。

在这些绝望的时刻,我们犯了一个经典的软件开发错误:将解决方案需求混淆。 实际需求(“三大要素”)没有改变。 围绕设计的环境发生了变化:AppDomain 不再可用。 当人们犯了将解决方案与需求混淆的错误时,他们可能会加倍下注,更紧地抓住方向盘,并且只是在试图强迫他们的解决方案继续工作时胡乱折腾。

相反,我们需要认识到显而易见的事实:我们有熟悉的需求,但是有新的环境,是时候抛弃旧的设计,采用新的设计,在新环境下满足相同的需求。 一旦我们允许自己回到绘图板,解决方案就很明确了

“三大要素”让您的“库”测试项目感觉像一个控制台应用程序。 那么,如果您的测试项目就是一个控制台应用程序呢?

控制台应用程序已经具有从正确的文件夹加载依赖项的有意义的概念,这与另一个应用程序的依赖项不同,同时尊重其自己的配置文件。 测试运行程序不再是混合环境中唯一的进程。 相反,测试运行程序的工作是启动测试项目作为其自身的进程,并与其通信以收集结果。 我们用进程间通信代替了 AppDomain,从而产生了一个新的设计,该设计满足了所有原始要求,同时也可以在 .NET Framework .NET Core 项目的上下文中工作。

Fixie version 2

这种设计使项目得以存活,并使我们能够在那些不确定的年代为两个平台提供服务,当时尚不确定哪个平台会在长期内幸存下来。 但是,维护对两个世界的支持变得越来越痛苦,尤其是在通过每个 Visual Studio 小版本更新保持 Visual Studio Test Explorer 插件的活力方面。 每个 Fixie 小版本都涉及大量的用例来进行回归测试,并且每一个新的小挫折都使创新停滞不前。

最重要的是,微软开始显示出明显的迹象,表明它正在放弃 .NET Framework:旧的 Framework 不再跟上 .NET Standard、ASP.NET 或 C# 的进步。 .NET Framework 将会存在,但会很快被淘汰。

情况再次发生了变化。

Fixie v3:拥抱 One .NET

Fixie v3 是一项正在进行中的工作,我们计划在 .NET 5 到达后不久发布。 .NET 5 是对 .NET Framework 与 .NET Core 开发路线的解决方案,最终形成了 One .NET。 我们没有与之抗争,而是遵循微软的演进:Fixie v3 将不再在 .NET Framework 上运行。 删除 .NET Framework 支持使我们能够删除许多旧的、缓慢的实现细节,并大大简化了我们必须考虑的每个版本的回归测试场景。 这也使我们能够重新考虑我们的设计。

“三大要素”要求仅略有变化:.NET Core 取消了与您的可执行文件紧密相关的 App.config 文件的概念,而是依赖于更基于约定的配置。 Fixie 的所有程序集加载要求仍然存在。 更重要的是,围绕设计的环境发生了根本性的变化:我们不再局限于使用 .NET Framework 和 .NET Core 中都可用的类型。

通过承诺减少 .NET Framework 支持,我们获得了现代化系统的新自由度。

.NET 的 AssemblyLoadContext 是 AppDomain 的远房表亲。 它不适用于 .NET Framework,因此以前对我们来说不是一个选择。 AssemblyLoadContext 允许您为程序集及其自身的依赖项设置专用加载区域,而不会污染周围的进程,并且不受限于原始进程自身的程序集文件夹。 换句话说,它提供了 AppDomain 的“将此程序集文件夹加载到侧面”行为,而没有令人沮丧的 AppDomain 怪癖。

我们定义了 TestAssemblyLoadContext 的概念,即一个测试程序集文件夹所需的小型程序集加载口袋

class TestAssemblyLoadContext : AssemblyLoadContext
{
    readonly AssemblyDependencyResolver resolver;

    public TestAssemblyLoadContext(string testAssemblyPath)
        => resolver = new AssemblyDependencyResolver(testAssemblyPath);

    protected override Assembly? Load(AssemblyName assemblyName)
    {
        // Reuse the Fixie.dll already loaded in the containing process.
        if (assemblyName.Name == "Fixie")
            return null;

        var assemblyPath = resolver.ResolveAssemblyToPath(assemblyName);

        if (assemblyPath != null)
            return LoadFromAssemblyPath(assemblyPath);

        return null;
    }

    ...
}

有了这个类,我们可以安全地从正确的文件夹中成功加载测试程序集及其所有依赖项。 测试运行程序可以直接使用加载的 Assembly,并且知道加载工作不会污染测试运行程序自身的依赖项

var assemblyName = new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath));
var testAssemblyLoadContext = new TestAssemblyLoadContext(assemblyPath);
var assembly = testAssemblyLoadContext.LoadFromAssemblyName(assemblyName);
// Use System.Reflection.* against `assembly` to find and run test methods...

我们已经完成了一个完整的循环:Fixie v3 Visual Studio 插件使用 TestAssemblyLoadContext 在进程中加载测试程序集,类似于 Fixie v1 插件使用 AppDomain 的方式。 核心 Fixie.dll 程序集只需要加载一次。 最重要的是,我们消除了所有进程间通信,同时利用了新环境允许的最佳功能。

Fixie version 3

始终进行设计

当您使用任何长期存在的系统时,您的一些维护痛点实际上是外部环境已发生变化的线索。 如果您的环境正在发生变化,请退后一步并重新考虑您的设计。 您是否将您的解决方案误认为是您的需求? 将您的需求与您的解决方案分开阐述,看看您的环境是否暗示了一个新的甚至令人兴奋的方向。

接下来阅读什么

微软加入开源阵营

上周三,微软宣布他们正在将其 .NET 平台的服务器端过渡到开源。 正如他们在网站上所述:微软正在提供完整的……

标签
User profile image.
Patrick 是 Fixie .NET 测试框架的创建者,也是 Visual Studio Test Platform 的贡献者。 他是 Headspring 的首席顾问,在那里他领导针对职业生涯早期的开发人员的培训和指导计划。

2 条评论

感谢这篇文章,它真的很有帮助。

您是否考虑过创建 ReSharper/Rider Testrunner? 它将如何适应这种情况,它将如何与 VS 运行程序进行比较?

这将大大有助于采用,尤其是在 .NET Core 促使越来越多的传统 Windows 开发人员使用其他平台作为其主要开发环境的情况下。 迄今为止,它造成了一些令人生畏的障碍。 最近,一个最小的路径似乎是可能的,因为你可以制作一个 R#/Rider 插件,该插件主要将工作委托给现有的 Visual Studio Test Explorer 运行程序,但即使那样,你也需要购买 JetBrains 插件的整个技术堆栈,所以即使是一个最小的插件也会有一个相当大的学习曲线。 我不想过度承诺,但我确实打算在 Fixie v3 接近发布时尝试一下。

回复 作者 Bruno Juchli (未验证)

© . All rights reserved.