编写可测试代码


引言

测试是 web 和软件开发的重要部分。它有助于提供信心,让你确信自己编写的任何代码都符合接受标准,并减少代码出现 bug 的可能性。实际上,TDD(测试驱动开发),一种受欢迎的开发方法,实际上主要关注的是在将任何代码添加到实际应用程序代码库之前编写测试。

目标读者

本篇教程主要面向的是那些对 Laravel 世界相当新的开发者,但对测试有基本了解。我们不会涵盖如何编写基本的测试,但将向你展示你可以以稍微不同的方式来处理代码,以提高你的代码质量和测试质量。

为什么我应该编写测试?

测试经常被认为是一种事后思考和"最好有"的东西,这种观念在那些业务目标和时间限制对开发团队施加压力的组织中尤其明显。如果你只是试图快速构建一个MVP(最小可行产品)或原型,也许测试可以稍微放在后面一点。但是,事实是,先编写测试再将代码发布到生产环境总是最好的选择!

当你编写测试时,你在做很多事情:

  • 早期发现 bug - 诚实点,你有多少次写了代码,运行了一两次,然后提交到你的版本控制系统呢?我举手承认,我自己也这么做过。你自己会想 "它看起来对,似乎可以运行,我确定它会没问题"。我每次这样做时,要么我的 pull 请求在 GitHub 上被拒绝,要么 bug 被发布到生产环境。因此,通过编写测试,你可以在你提交工作之前发现 bug,并在将它们发布到生产环境时有更多的信心。
  • 使未来的工作和重构更容易 - 想象一下,你需要重构应用程序的 core 类。或者,你可能需要在该类中添加一些新的代码以扩展功能。没有测试,你怎么能确定修改或添加任何代码都不会破坏现有的功能呢?没有大量的手动测试,我们很难快速检查。所以,当你编写第一个版本的代码时,你可以编写测试,将它们当作回归测试。这意味着,每次你更新任何代码,你都可以运行测试来确保一切都还在正常工作。你也可以继续添加测试,每次你添加新的代码时,你都可以确保你的添加也在正常工作。
  • 改变你编写代码的方式 - 当我第一次学习测试并开始编写我的第一个测试(对于一个用 PHPUnit 编写的 Laravel 应用程序)时,我很快就意识到我的代码编写测试非常困难。像模拟类,防止第三方 API 调用和做一些断言这样的事情很难。要能够编写一种可以被测试的代码,你必须沿着比以前略微不同的角度来查看你的类和方法的结构。

编写控制器测试

为了解释我们如何使你的代码更易于测试,我们将使用一个简单的示例。当然,你可以用不同的方式编写代码,这可能就那么简单,根本不重要。但是,希望这可以帮助解释整个概念。

让我们看看这个例子的控制器方法:

image-20230831172036305

上述方法,我们假设如果你对 /newsletter/subscriptions 发起 POST 请求时将被调用,它接受一个电子邮件参数,然后传递给服务。我们可以假设服务处理完成用户订阅新闻通讯所需的所有不同流程。

为了测试上述控制器方法,我们可以创建以下测试:

image-20230831172105156

在我们的测试中,你可能已经注意到一个问题。它实际上并未检查服务类的 handle() 方法是否被调用!所以,如果我们不小心删除或注释掉控制器中的那一行,我们实际上是不会知道的。

编写更好的控制器测试

问题在哪里?

我们这里的一个问题是,如果不添加额外的代码来标记或记录服务类被调用,那么我们实际上很难检查它是否已经被编写。

当然,我们可以在这个控制器测试中添加更多的断言来测试服务类的所有代码是否都在运行。但这可能会导致你的测试重叠。为了争论的份,假设我们的 Laravel 应用允许用户注册,并且每当他们注册时,他们都会自动注册新闻通讯。现在,如果我们也为这个控制器编写测试,那么检查所有的服务类是否正确运行,我们会有两份近乎相同的测试代码。这意味着,如果我们需要更新服务类的内部运行方式,我们还需要更新所有这些测试。

但是,公正来说,有时候你可能实际上想要这么做。如果你正在编写一个功能测试并针对整个端到端过程进行断言,这将是合适的。然而,如果你试图编写单元测试并仅想检查控制器,这种方法就不太管用了。

我们如何解决问题?

为了改进我们拥有的测试,我们可以使用模拟服务容器依赖注入。我不会过多地深入讲解服务容器是什么,但是,我强烈建议阅读它,因为它可以非常有帮助,是 Laravel 的核心部分。

简而言之(用非常基础的术语),服务容器管理类依赖关系,并允许我们使用 Laravel 已经为我们设置的类。为了理解我这么说的意思,让我们看看下面的例子。

为了使我们的代码示例更易于测试,我们可以使用依赖注入来解析来自服务容器的 NewsletterSubscriptionService ,就像这样:

image-20230831172427069

在上面,我们在 store() 方法中添加了 NewsletterSubscriptionService 类作为一个参数,因为 Laravel 允许在控制器中使用依赖注入。这基本上是告诉 Laravel 当它调用此方法时 "嘿,我还希望你传给我一个 NewsletterSubscriptionService !"。然后 Laravel 回应说 "好的,我现在就从服务容器里为你找一个"。

在这种情况下,我们的服务类没有任何构造函数参数,所以很简单。然而,如果我们需要传入构造函数参数,我们可能需要创建一个服务提供者,该服务提供者处理在我们首次实例化类时传入的数据。

因为我们现在从容器中解析,我们可以像这样更新我们的测试:

image-20230831172457559

现在,在上面的测试中,我们首先使用 Mockery 创建了一个服务类的模拟。然后我们告诉服务类,在测试完成运行之前,我们期望 handle() 方法被调用一次,并且只有一个参数 mail@ashallendesign.co.uk 。在做完那些之后,我们就告诉 Laravel "嘿,Laravel,如果你需要解析一个 NewsletterSubscriptionService ,这里有一个可以返回的"。

这意味着现在在我们的控制器中,第二个参数实际上不是服务类本身,而是此类的模拟版本。

所以,当我们现在运行测试,我们会看到 handle() 方法实际上是被调用的。因此,如果我们曾经删除调用该代码的位置,或者添加任何可能阻止它被调用的逻辑,测试将会失败,因为 Mockery 会检测到该方法没有被调用。

额外的测试技巧

有时候,当你在一段代码的内部时,发现如果不进行大规模的重构,你将无法通过将其作为额外的方法参数来注入你想要模拟的类。在这些情况下,你可以利用 Laravel 提供的 resolve() 辅助函数。

resolve() 函数简单地从服务容器返回一个类。作为一个小例子,让我们看看我们如何可以更新我们的示例控制器方法,以使其能够用 Mockery 进行测试,但无需添加额外的参数:

image-20230831173122359

小结

这篇教程给你一些关于你如何通过利用服务容器,模拟和依赖注入来使你的 Laravel 应用更易测试的启示。记住,测试是你的好朋友,如果代码在第一次编写时添加测试,它们可以为你节省大量的时间,减少压力和压力。作为一个额外的奖励,高质量的测试和增加的测试覆盖率通常(但并不总是)会导致更少的 bug,这意味着更少的支持票和更开心的客户!


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 使用策略模式

>> 下一篇: 没有下一篇了