如何在Spring Boot的主干或测试驱动的开发应用程序中构建金字塔

经常将Spring框架作为Cloud Native框架的示例,该框架旨在在云中工作,开发十二要素应用程序 ,微服务以及最稳定的但同时也是创新产品之一。 但是在本文中,我想谈谈Spring的另一个强项:它是通过测试提供的开发支持(TDD功能?)。 尽管具有TDD连接性,但我经常注意到Spring项目要么忽略了一些最佳测试实践,要么发明了自己的自行车,要么根本不编写测试,因为它们“慢”或“不可靠”。 而且,我将确切告诉您如何在Spring Framework上为应用程序编写快速 可靠的测试,以及如何通过测试进行开发。 因此,如果您使用Spring(或想要开始),则了解一般的测试(或想要了解),或者认为contextLoads是集成测试的必要和充分级别-这将很有趣!


“ TDD”功能非常模棱两可,很难测量,但是尽管如此,Spring还是有很多东西可以通过设计帮助以最小的努力编写集成和单元测试。 例如:


  • 集成测试-您可以轻松启动应用程序,锁定组件,重新定义参数等。
  • 焦点集成测试-仅访问数据,仅访问Web等
  • 开箱即用的支持-内存数据库,消息队列,测试中的身份验证和授权
  • 通过合同进行测试(Spring Cloud合同)
  • 使用HtmlUnit的Web UI测试支持
  • 应用程序配置的灵活性-配置文件,测试配置,组件等
  • 还有更多

首先,简要介绍了TDD和测试。


测试驱动开发


TDD基于一个非常简单的想法-我们编写代码之前先编写测试。 从理论上讲,这听起来很吓人,但是一段时间后,人们对实践和技术有了理解, 以后再编写测试的选择会引起明显的不适。 关键实践之一是迭代 ,即 使所有事情都变得小而集中,每次迭代都被描述为Red-Green-Refactor


红色阶段,我们编写了一个下降测试,非常重要的一点是,下降测试应具有清晰,可理解的原因和描述,并且测试本身是完整的并在编写代码时通过。 测试应该检查行为 ,而不是实现 ,即 遵循黑匣子方法,那么我将解释原因。


绿色阶段,我们编写了通过测试所需最少代码 。 有时候,进行练习并使其变得疯狂起来是很有趣的(尽管最好不要被迷惑),并且当一个函数根据系统的状态返回布尔值时,第一个“ pass”可能只是return true


重构阶段( 只有在所有测试都为绿色时才能启动),我们将重构代码并将其置于适当的状态。 我们编写的代码甚至都不需要,因此在稳定的系统上开始重构很重要。 “黑匣子”方法将仅有助于重构,更改实现,但不会影响行为。


将来,我将讨论TDD的不同方面,毕竟,这是一系列文章的想法,因此,现在我不再特别关注细节。 但是在回应标准TDD批评之前,我会先提一些我经常听到的神话。


  • “ TDD大约是代码的100%覆盖率,但不能保证” -通过测试进行开发与100%覆盖率根本没有关系。 在我工作过的许多团队中,该指标甚至没有进行衡量,因此被归类为虚荣指标。 是的,100%的测试覆盖率毫无意义。
  • “ TDD仅适用于简单的功能,带有数据库的真实应用程序无法使用它来完成困难状态”是一个非常受欢迎的借口,通常以“我们拥有如此复杂的应用程序,根本不编写测试,这是不可能的”作为补充。 我看到了在完全不同的应用程序上有效的TDD方法-Web(使用和不使用SPA),移动,API,微服务,整体结构,复杂的银行系统,云平台,框架,用不同语言和技术编写的零售平台。 因此,流行的神话“我们是独一无二的,一切都是不同的”通常是没有在测试上投入精力和金钱的借口,但这不是真正的理由(尽管也可能有真正的理由)。
  • 当然,与其他软件一样,TDD仍然存在错误” 。 TDD与错误或根本不存在有关,它是一种开发工具。 喜欢调试。 就像一个IDE。 像文档一样。 这些工具都不能保证没有错误,它们只能帮助解决系统日益复杂的问题。

TDD和一般测试的主要目标是使团队确信该系统运行稳定。 因此,没有一种测试实践可以确定要编写多少个和哪个测试。 写出您认为必要的代码量,以及确保现在可以将代码投入生产并能正常工作所需的代码 。 有些人认为快速集成测试是必要且足够的最终黑匣子,而单元测试则是可选的。 有人说e2e测试可能会快速回滚到以前的版本,而canary版本的存在并不是那么关键。 多少团队-如此众多的方法,找到自己的团队很重要。

我的目标之一是摆脱TDD故事中“通过测试将两个数字相加的函数进行开发”的格式,而转向真正的应用程序,这种测试实践已被蒸发到最小的应用程序,并收集到实际项目中。 作为一个半真实的示例,我将使用我自己发明的一个小型Web应用程序,用于抽象 工厂 面包糕点厂 。 我打算写一些小文章,每次都专注于单独的应用程序功能,并通过TDD展示您可以设计API,应用程序的内部结构并保持不变的重构。


我现在看到的一系列文章的示例计划是:


  1. 行走骨架-可在其中运行Red-Green-Refactor循环的应用程序框架
  2. UI测试和行为驱动设计
  3. 数据访问测试(春季数据)
  4. 授权和认证测试(Spring Security)
  5. Jet Stack(WebFlux +项目反应堆)
  6. (微)服务和合同的互操作性(Spring Cloud)
  7. 测试消息队列(Spring Cloud)

这篇介绍性文章将涉及第1点和第2点-我将使用BDD或行为驱动的开发方法创建一个应用程序框架和一个基本的UI测试。 每篇文章都将从用户故事开始,但是为了节省时间,我不会谈论“产品”部分。 用户故事将用英语编写,很快就会明白原因。 所有代码示例都可以在GitHub上找到,因此我将不分析所有代码,仅分析重要部分。


用户故事是对自然语言应用程序功能的描述,通常是代表系统用户编写的。

用户故事1:用户看到欢迎页面


作为 Alice,一个新用户
访问Cake Factory网站时, 我想看到一个欢迎页面
这样我就知道Cake Factory何时启动

验收标准:
方案:在启动日期之前访问网站的用户
鉴于我是新用户
当我访问Cake Factory网站时
然后我看到一条消息“感谢您的关注”
我看到一条消息“该网站即将推出...”

这将需要知识:什么是行为驱动的开发黄瓜Spring Boot Testing的基础。


第一个用户故事是非常基本的,但是目标不是复杂性,而是创建行走骨骼 -启动TDD周期的最小应用程序。


在带有Web和Mustache模块的Spring Initializr上创建了一个新项目之后,首先,我需要对build.gradle进行一些更改:


  • 添加HtmlUnit testImplementation('net.sourceforge.htmlunit:htmlunit') 。 您无需指定版本,Gradle的Spring Boot依赖管理插件将自动选择必要的兼容版本
  • 将项目从JUnit 4迁移到JUnit 5(因为已经到了2018年)
  • 向Cucumber添加依赖项-我将用来编写BDD规范的库
  • 删除默认情况下带有不可避免的contextLoads创建的contextLoads

总的来说,这是应用程序的基本“骨架”,您已经可以编写第一个测试。


为了使浏览代码更容易,我将简要讨论所使用的技术。


黄瓜


Cucumber是一个行为驱动的开发框架,可帮助创建“可执行规范”,即 运行以自然语言编写的测试(规范)。 Cucumber插件使用Java(和许多其他语言)解析源代码,并使用步骤定义来运行真实代码。 步骤定义是用@Given@When@Then和其他注释注释的类方法。


单位


项目主页将HtmlUnit称为“ Java应用程序的无GUI浏览器”。 与Selenium不同,HtmlUnit不会启动真正的浏览器,最重要的是,它根本无法渲染页面,而是直接与DOM一起工作。 Mozilla Rhino引擎支持JavaScript。 HtmlUnit非常适合经典应用程序,但与单页应用程序不太友好。 首先,就足够了,然后我将尝试证明即使将诸如测试框架之类的东西也可以作为实现的一部分,而不是应用程序的基础。


第一次测试


现在,用英语写的用户故事将对我派上用场。 开始下一个TDD迭代的最佳触发器是接受标准,其编写方式应使它们可以用最少的手势转换为可执行规范。


理想情况下,应该编写用户故事,以便可以将它们简单地复制到BDD规范中并运行。 这远非总是简单且并非总是可能的,但这应该是产品所有者和整个团队的目标,尽管并非总是可以实现的。

所以,我的第一个功能。


 Feature: Welcome page Scenario: a user visiting the web-site visit before the launch date Given a new user, Alice When she visits Cake Factory web-site Then she sees a message 'Thank you for your interest' And she sees a message 'The web-site is coming in December!' 

如果您生成步骤说明( Intellij IDEA插件对Gherkin的支持有很大帮助)并运行测试,那么它当然是绿色的-它尚未进行任何测试。 接下来是进行测试的重要阶段- 您需要编写测试,就像编写主代码一样


通常,对于那些开始放弃TDD的人来说,这里是一个昏昏欲睡的地方-很难把不存在的事物的算法和逻辑放在首位。 因此,从用户故事开始一直到集成和单元级别,有尽可能小的且集中的迭代非常重要。 重要的是,一次只专注于一项测试,并尝试弄湿并忽略尚不重要的依赖项。 我有时会注意到人们是如何轻松地离开的-为依赖项创建一个接口或类,立即为其生成一个空的测试类,在此添加一个依赖项,再创建一个接口,依此类推。


如果故事是“保存时有必要刷新状态”,则很难实现自动化和形式化。 在我的示例中,每个步骤都可以按可以用代码描述的一系列步骤清楚地列出。 显然,这是最简单的示例,它并没有显示太多,但是我希望,随着复杂性的增加,它会变得更加有趣。

红色的


因此,对于我的第一个功能,我创建了以下步骤描述:


 @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WelcomePage { private WebClient webClient; private HtmlPage page; @LocalServerPort private int port; private String baseUrl; @Before public void setUp() { webClient = new WebClient(); baseUrl = "http://localhost:" + port; } @Given("a new user, Alice") public void aNewUser() { // nothing here, every user is new by default } @When("she visits Cake Factory web-site") public void sheVisitsCakeFactoryWebSite() throws IOException { page = webClient.getPage(baseUrl); } @Then("she sees a message {string}") public void sheSeesAMessageThanksForYourInterest(String expectedMessage) { assertThat(page.getBody().asText()).contains(expectedMessage); } } 

需要注意的几点:


  • 功能由另一个文件Features.java使用JUnit 4中的RunWith注释启动,Cucumber不支持版本5,a
  • @SpringBootTest批注被添加到步骤的描述中, cucumber-spring从那里进行选择并配置测试上下文(即,启动应用程序)
  • Spring的测试应用程序以webEnvironment = RANDOM_PORT ,此随机端口使用@LocalServerPort传递给测试,Spring会找到此批注并将字段值设置为服务器端口

正如预期的那样,该测试404 for http://localhost:51517错误404 for http://localhost:51517崩溃。


测试崩溃的错误非常重要,尤其是在单元测试或集成测试时,这些错误是API的一部分。 如果测试因NullPointerException崩溃NullPointerException则效果不是很好,但是BaseUrl configuration property is not set -更好。

绿色的


为了使测试变为绿色,我添加了一个基本控制器,并使用最少的HTML进行了查看:


 @Controller public class IndexController { @GetMapping public String index() { return "index"; } } 

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Cake Factory</title> </head> <body> <h1>Thank you for your interest</h1> <h2>The web-site is coming in December!</h2> </body> </html> 

该测试是绿色的,可以应用,尽管它是按照严格的工程设计传统进行的。


当然,在一个真实的项目中和一个平衡的团队中,我将与设计师坐下来,我们将把裸HTML变成更漂亮的东西。 但在本文的框架内,奇迹不会发生,公主将依然是青蛙。

“ TDD的一部分是设计”这个问题并不是那么简单。 我发现有用的一种做法是,一开始甚至根本不查看UI(甚至不运行应用程序来安心),编写测试,使其变为绿色-然后,在稳定的基础上进行前端工作,不断重新启动测试。 。


重构


在第一个迭代中,没有特定的重构,但是尽管我花了最后10分钟为Bulma选择一个模板,这可以算作重构!


总结


尽管应用程序既没有安全性工作,也没有数据库,也没有API,但是测试和TDD看起来很简单。 通常,在测试金字塔中,我只涉及最高级的UI测试。 但是,在某种程度上,精益方法的秘密在于以小的迭代方式完成所有工作,一次只做一个组件。 这有助于集中精力进行测试,使其变得简单,并控制代码的质量。 我希望在接下来的文章中会有更多有趣的内容。


参考文献



附言:我想许多人已经猜到了,文章的标题并不像开始时那样疯狂。 “如何在靴子中构建金字塔”是对测试金字塔(稍后将向您详细介绍)和Spring Boot的引用,在Spring Boot中,英式英语中的靴子也表示“树干”。

Source: https://habr.com/ru/post/zh-CN431306/


All Articles