Skip to content

搞英语 → 看世界

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

Menu
  • 首页
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
  • Product Hunt
  • Visual Capitalist
  • Elon Musk
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

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Articles on Jose M. (2)
  • Balaji S. Srinivasan (1)
  • Blogs on Dev.Poga (1)
  • Bob Nystrom (1)
  • Dan Wang (1)
  • dostoynikov (4)
  • Joel on Software (1)
  • John Resig (1)
  • Laurence Gellert's Blog (1)
  • Matt Might's blog (4)
  • Michael Feathers (1)
  • News Letter (206)
  • Noahpinion (11)
  • Philip Walton (1)
  • Pivotal (1)
  • Rohit Kumar (1)
  • Sam Julien (1)
  • Scott Hanselman's Blog (2)
  • ShinChven (3)
  • Tom's blog (2)
  • Wait But Why (2)
  • 湾区日报 (22)
  • 英文媒体 (40,694)
    • Ars Technica (2,827)
    • Daily Infographic (338)
    • Engadget (6,220)
    • Enonomist (77)
    • FlowingData (280)
    • Hacker News (773)
    • Hacker News Daily (353)
    • Hacker Noon (125)
    • Harvard Health (155)
    • KK – Cool Tools (245)
    • KK – Recomendo (279)
    • Make Use Of (158)
    • NASA Astronomy Picture (311)
    • Product Hunt (8,609)
    • Psyche (236)
    • Quanta Magazine (202)
    • Science current issue (668)
    • Sidebar (1,165)
    • Singularity HUB (307)
    • TechCrunch (9,929)
    • The Practical Developer (99)
    • The Verge (6,913)
    • Visual Capitalist (425)
  • 英文推特 (17,685)
    • Bill Gates (342)
    • Brett Winton (1,333)
    • Cathie Wood (303)
    • Durov's Channel (25)
    • Elon Musk (5,422)
    • GeekWire (2,868)
    • Hunter Walk (57)
    • Mark Gurman (1,061)
    • Naval (699)
    • Parag Agrawal (52)
    • Ray Dalio (1,045)
    • Riccardo Mori (16)
    • Steph Smith (2,269)
    • Tim Cook (169)
    • Vitalik Buterin (2,024)
  • 英文独立博客 (4,262)
    • A learning a day (355)
    • A Smart Bear (2)
    • AddyOsmani.com (11)
    • Adwyat Krishna (30)
    • Ahmad Shadeed (2)
    • Alex Turek (3)
    • All Poetry (1)
    • All That is Solid (51)
    • André Staltz (4)
    • arxivblog (37)
    • Astral Codex Ten (15)
    • Atoms vs Bits (29)
    • AVC (45)
    • Basic Apple Guy (44)
    • Ben Thompson (13)
    • Benedict Evans (8)
    • Blog – storytelling with data (49)
    • Built For Mars (12)
    • Caleb Porzio (1)
    • Cameron Sun (2)
    • Christian Heilmann (43)
    • Christopher C (3)
    • Chun Tian (binghe) (1)
    • Codrops (23)
    • Cold Takes (16)
    • Dan Luu (1)
    • Daniel Lemire's blog (66)
    • David Amos (23)
    • David Perell (6)
    • David Walsh Blog (43)
    • Derek Sivers (30)
    • Desvl (16)
    • Devon's Site (5)
    • Digital Inspiration (28)
    • DKB Blog (4)
    • Douglas Vaghetti (12)
    • dropsafe (65)
    • DSHR (41)
    • Dunk (5)
    • DYNOMIGHT (41)
    • eagereyes (7)
    • Endless Metrics (135)
    • Entitled Opinions (8)
    • Exception Not Found (6)
    • Experimental History (26)
    • Farnam Street (6)
    • Fed Guy (12)
    • Felix Krause (3)
    • Florent Crivello (2)
    • Free Mind (7)
    • Full Stack Economics (40)
    • Funny JS (3)
    • Future A16Z (47)
    • Glassnode Insights (62)
    • Human Who Codes (5)
    • Infographics – Cool Infographics (12)
    • Information is Beautiful (13)
    • Irrational Exuberance (46)
    • Jacob Kaplan-Moss (13)
    • Jakob Greenfeld (51)
    • James Sinclair (3)
    • Jason Fried (21)
    • Jeff Kaufman (198)
    • John's internet house (31)
    • Johnny Rodgers (4)
    • Julia Evans (27)
    • Julian.com (2)
    • Kalzumeus (1)
    • Kevin Cox (13)
    • Kevin Norman (4)
    • KK – The Technium (51)
    • Krishna (7)
    • Lee Robinson (5)
    • Lines and Colors (53)
    • Lyn Alden – Investment Strategy (3)
    • Martin Fowler (30)
    • More To That (17)
    • Morgan Housel (82)
    • My Super Secret Diary (32)
    • Naval Blog (3)
    • Neckar's New Money (88)
    • Nick Whitaker (4)
    • Nicky's New Shtuff (1)
    • nutcroft (13)
    • Paul Graham (2)
    • Paul Graham: Essays (2)
    • Penguin Random House (91)
    • Phoenix's island (1)
    • Prof Galloway (37)
    • Python Weekly (35)
    • Rachel (36)
    • Real Life (34)
    • Sasha (68)
    • Science & technology (134)
    • Sébastien Dubois (6)
    • Secretum Secretorum (14)
    • Seth's Blog (188)
    • Shu Ding (3)
    • SignalFire (10)
    • Simon Willison's Weblog (255)
    • Simons Foundation (89)
    • SLIME MOLD TIME MOLD (25)
    • Slyar Home (8)
    • Spencer Greenberg (12)
    • Stay SaaSy (14)
    • Stephen Malina (4)
    • Stephen Wolfram Writings (2)
    • Strange Loop Canon (25)
    • Stratechery (11)
    • Tech Notes (11)
    • The Commonplace (30)
    • The Generalist (2)
    • The Intrinsic Perspective (31)
    • The Latest in Hearing Health | HeardThat (8)
    • The Mad Ned Memo (2)
    • The Rabbit Hole (38)
    • TLDR Newsletter (81)
    • Tomasz Tunguz (99)
    • Tony Kulesa (2)
    • Troy Hunt (68)
    • Tychlog (1)
    • Uncharted Territories (66)
    • Visualising Data (10)
    • Weichen Liu (21)
    • What's New (57)
    • Works in Progress (1)
    • Workspaces (32)
    • Writing (9)
    • Xe's Blog (42)
    • xkcd.com (134)
    • Yihui Xie (17)
    • Zoran Jambor (11)
©2023 搞英语 → 看世界 | Design: Newspaperly WordPress Theme