Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

Angular 和 Spring Boot – 使用 Serenity BDD 进行集成测试

Posted on 2022-05-25

介绍

使用 Angular 和 Spring Boot 组合构建现代 Web 应用程序在大型和小型企业中都非常流行。 Angular 提供了所有必要的工具来构建一个健壮、快速和可扩展的前端,而 Spring Boot 为后端完成了同样的工作,而无需配置和维护 Web 应用程序服务器。

\ 确保构成最终产品的所有软件组件协同工作,它们必须一起进行测试。这就是使用 Serenity BDD 进行集成测试的地方。Serenity BDD 是一个开源库,有助于编写更清晰、更可维护的自动化验收和回归测试。

\

:::info BDD – 行为驱动开发是一种测试技术,涉及以简单的以业务为中心的语言表达应用程序的行为方式。

:::

\

目标

本文的目标是构建一个简单的 Web 应用程序,该应用程序试图根据一个人的名字来预测他们的年龄。然后,使用 Serenity BDD 库编写一个集成测试,以确保应用程序正常运行。

\

构建 Web 应用程序

首先,重点将放在 Spring Boot 后端。将使用 Spring RestController 公开 GET API 端点。当使用人名调用端点时,它将返回该名称的预测年龄。实际预测将由agify.io处理。

\ 接下来,将实现一个向用户呈现文本输入的 Angular 应用程序。当在输入中输入名称时,将向后端触发 HTTP GET 请求以获取年龄预测。然后应用程序将接受预测,并将其显示给用户。

\

:::tip 本文的完整项目代码可在GitHub 上获取

:::

\

构建后端

首先定义年龄预测模型。它将采用带有name和age的 Java 记录的形式。此处还将定义一个空的年龄预测:

\ AgePrediction.java

 public record AgePrediction(String name, int age) { private AgePrediction() { this("", 0); } public static AgePrediction empty() { return new AgePrediction(); } }

RestController 处理对/age/prediction的 HTTP 调用。它定义了一个 GET 方法,该方法接收名称并访问api.agify.io以获取年龄预测。该方法使用@CrossOrigin注释以允许来自 Angular 的请求。如果未提供name参数,则该方法仅返回一个空的年龄预测。

\ 为了对预测进行实际调用,将使用 Spring 的 REST 客户端 — RestTemplate:

\ AgePredictionController.java

 @RestController @RequestMapping("/age/prediction") @RequiredArgsConstructor public class AgePredictionController { private final static String API_ENDPOINT = "https://api.agify.io"; private final RestTemplate restTemplate; /** * Tries to predict the age for the provided name. * * If name is empty, an empty prediction is returned. * * @param name used for age prediction * @return age prediction for given name */ @CrossOrigin(origins = "http://localhost:4200") @GetMapping public AgePrediction predictAge(@RequestParam(required = false) String name) { if (StringUtils.isEmpty(name)) { return AgePrediction.empty(); } HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); HttpEntity<?> entity = new HttpEntity<>(headers); return restTemplate.exchange(buildAgePredictionForNameURL(name), HttpMethod.GET, entity, AgePrediction.class).getBody(); } private String buildAgePredictionForNameURL(String name) { return UriComponentsBuilder .fromHttpUrl(API_ENDPOINT) .queryParam("name", name) .toUriString(); } }

\

构建前端

年龄预测模型将被定义为具有name和age的接口:

\年龄预测.model.ts

 export interface AgePredictionModel { name: string; age: number; }

该网页将包含一个文本<input> ,用户将在其中键入用于年龄预测的姓名,以及两个<h3>元素,其中将显示姓名和预测年龄。

当用户输入<input>时,文本将通过onNameChanged($event)函数传递给 typescript 类。

\ 显示name和预测age是通过订阅agePrediction$ observable 来处理的。

\ app.component.html

 <div> <label>Enter name to get age prediction: </label> <input id="nameInput" type="text" (input)="onNameChanged($event)"/> </div> <div> <h3> Name: <span id="personName"></span> </h3> </div> <div> <h3> Age: <span id="personAge"></span> </h3> </div>

至于 Angular 组件,它会在<input>上通过函数onNameChanged($event)发生变化时被调用。该事件被转换为一个名为agePrediction$的可观察对象,通过管道将 HTTP GET 发送到具有最新名称的后端。这是通过使用 Subject nameSubject和 RxJs 运算符 debounceTime、distinctUntilChanged、switchMap、shareReplay 来实现的。

\

:::信息

  • debounceTime – 仅在经过特定时间跨度且没有另一个源发射后才从源 Observable 发射一个值
  • distinctUntilChanged – 如果源 observable 推送的所有值与 observable 发出的最后一个值相比是不同的,则发出它们
  • switchMap – 将每个源值投影到一个 Observable 中,该 Observable 合并到输出 Observable 中,仅从最近投影的 Observable 发出值
  • shareReplay – 共享源并在订阅时重播指定数量的排放

:::

\ app.component.ts

 @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { static readonly AGE_PREDICTION_URL = 'http://localhost:8080/age/prediction'; agePrediction$: Observable<AgePredictionModel>; private nameSubject = new Subject<string>(); constructor(private http: HttpClient) { } ngOnInit() { this.agePrediction$ = this.nameSubject.asObservable().pipe( debounceTime(300), distinctUntilChanged(), switchMap(this.getAgePrediction), shareReplay() ); } /** * Fetches the age prediction model from our Spring backend. * * @param name used for age prediction */ getAgePrediction = (name: string): Observable<AgePredictionModel> => { const params = new HttpParams().set('name', name); return this.http.get<AgePredictionModel>(AppComponent.AGE_PREDICTION_URL, {params}); } onNameChanged($event) { this.nameSubject.next($event.target.value); } }

\ 年龄预测页面预览:

\ 年龄预测页面预览

\

编写集成测试

作为测试 Web 应用程序的第一步,创建一个抽象测试类来封装 Serenity 测试所需的逻辑:

\

  • Actor 代表使用被测应用程序的人或系统——这里简称为tester
  • WebDriver 是一个用于控制网络浏览器的接口。通过指定@Managed注解,Serenity 会将默认配置的实例注入browser
  • 在setBaseUrl()方法中,用于所有测试的基本 URL 是在 Serenity 的 EnvironmentVariables 中配置的。这是为了避免重复每个测试页面的协议、主机和端口

\ AbstractIntegrationTest.java

 public abstract class AbstractIntegrationTest { @Managed protected WebDriver browser; protected Actor tester; private EnvironmentVariables environmentVariables; @BeforeEach void setUp() { tester = Actor.named("Tester"); tester.can(BrowseTheWeb.with(browser)); setBaseUrl(); } private void setBaseUrl() { environmentVariables.setProperty(WEBDRIVER_BASE_URL.getPropertyName(), "http://localhost:4200"); } }

为了测试年龄预测页面,创建了一个继承自 PageObject(浏览器中的页面表示)的新 IndexPage 类。相对于先前指定的基本 URL 的页面 URL 是使用@DefaultUrl注释定义的。

页面上的 HTML 元素使用 Serenity Screenplay 流畅地定义。

\ IndexPage.java

 @DefaultUrl("/") public class IndexPage extends PageObject { public static final Target NAME_INPUT = the("name input").located(By.id("nameInput")); public static final Target PERSON_NAME = the("name header text").located(By.id("personName")); public static final Target PERSON_AGE = the("age header text").located(By.id("personAge")); }

最后,编写集成测试意味着继承自 AbstractIntegrationTest 的类,并使用 JUnit 的@ExtendWith和 Serenity 的 JUnit 5 扩展进行注释。 indexPage将在测试运行时由 Serenity 注入。在 BDD 方式中,测试以 given-when-then 块的形式构建。

\ 阅读测试试图达到的目标几乎和阅读简单的英语一样简单:

\

  • ‘given’ 语句将尝试在年龄预测页面上打开浏览器。

  • ‘when’ 语句将获取<input>的句柄并输入文本“Andrei”。

  • ‘then’ 语句将评估 4 个语句:

  • 验证人名<h3>在页面上是否可见

  • 验证页面上显示的人名是否是预期的人名

  • 验证人年龄<h3>在页面上是否可见

  • 验证人的年龄是否是一个数字(不检查固定年龄,因为年龄预测可能会改变)

    \

eventually通过在通过/失败测试条件之前等待 5 秒来适应较慢的后端响应。

\ IndexPageTest.java

 @ExtendWith(SerenityJUnit5Extension.class) public class IndexPageTest extends AbstractIntegrationTest { private static final String TEST_NAME = "Andrei"; private IndexPage indexPage; @Test public void givenIndexPage_whenUserInputsName_thenAgePredictionIsDisplayedOnScreen() { givenThat(tester).wasAbleTo(Open.browserOn(indexPage)); when(tester).attemptsTo(Enter.theValue(TEST_NAME).into(NAME_INPUT)); then(tester).should( eventually(seeThat(the(PERSON_NAME), isVisible())), eventually(seeThat(the(PERSON_NAME), containsText(TEST_NAME))), eventually(seeThat(the(PERSON_AGE), isVisible())), eventually(seeThat(the(PERSON_AGE), isANumber())) ); } private static Predicate<WebElementState> isANumber() { return (htmlElement) -> htmlElement.getText().matches("\\d*"); } }

\

概括

本文简要介绍了如何使用 Serenity BDD 来实现现代 Web 应用程序的集成测试。执行测试所需的配置量保持在最低限度,用于测试网页的生成代码阅读起来非常愉快,以至于您想知道它是如何工作的!

\

:::info我不是由上面列出的任何产品/服务/公司赞助或收到任何补偿。本文仅供参考。

:::

\

参考

  • https://serenity-bdd.github.io/theserenitybook/latest/index.html
  • https://medium.com/javascript-scene/behavior-driven-development-bdd-and-functional-testing-62084ad7f1f2
  • https://github.com/serenity-bdd/serenity-core
  • https://agify.io/
  • https://rxjs.dev/api/operators/

\

原文: https://hackernoon.com/angular-and-spring-boot-using-serenity-bdd-for-integration-testing?source=rss

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme