Angular и Spring Boot — использование Serenity BDD для интеграционного тестирования
25 мая 2022 г.Вступление
Создание современных веб-приложений с использованием комбинации Angular и Spring Boot очень популярно как среди крупных, так и среди малых предприятий. Angular предоставляет все необходимые инструменты для создания надежного, быстрого и масштабируемого внешнего интерфейса, в то время как Spring Boot делает то же самое для внутреннего интерфейса без хлопот с настройкой и обслуживанием сервера веб-приложений.
Чтобы убедиться, что все программные компоненты, из которых состоит конечный продукт, работают в унисон, их необходимо тестировать вместе. Вот тут-то и начинается интеграционное тестирование с Serenity BDD. Serenity BDD — это библиотека с открытым исходным кодом, которая помогает писать чище и удобнее в сопровождении. автоматические приемочные и регрессионные тесты.
:::Информация
BDD (Behaviour-Driven Development) — это метод тестирования, который включает в себя выражение того, как приложение должно вести себя, на простом бизнес-ориентированном языке.
Цель
Цель этой статьи — создать простое веб-приложение, которое пытается предсказать возраст человека по его имени. Затем, используя библиотеку Serenity BDD, напишите интеграционный тест, который гарантирует правильное поведение приложения.
Создание веб-приложения
Во-первых, основное внимание будет уделено серверной части Spring Boot. Конечная точка GET API будет отображаться с помощью Spring RestController. Когда конечная точка вызывается с именем человека, она возвращает предсказанный возраст для этого имени. Фактический прогноз будет обрабатываться agify.io.
Далее будет реализовано приложение Angular, предоставляющее пользователю ввод текста. Когда имя вводится во входные данные, на серверную часть будет отправлен HTTP-запрос GET для получения предсказания возраста. Затем приложение примет прогноз и отобразит его пользователю.
Полный код проекта для этой статьи доступен на [GitHub] (https://github.com/andreistefanwork/angular-spring-integration-test).
Создание бэкенда
Сначала будет определена модель прогнозирования возраста. Он будет иметь форму записи Java с именем
и возрастом
. Здесь также будет определен пустой прогноз возраста:
AgePrediction.java
```java
общедоступная запись AgePrediction (имя строки, целочисленный возраст) {
частный AgePrediction () {
это("", 0);
public static AgePrediction empty() {
вернуть новый AgePrediction();
RestController обрабатывает HTTP-вызовы к /age/prediction
. Он определяет метод GET, который получает имя и обращается к api.agify.io для получения прогноза возраста. Метод помечен @CrossOrigin
, чтобы разрешить запросы от Angular. Если параметр name
не указан, метод просто возвращает пустой прогноз возраста.
Чтобы сделать фактический вызов прогноза, будет использоваться REST-клиент Spring — RestTemplate:
AgePredictionController.java
```java
@RestController
@RequestMapping("/возраст/прогноз")
@RequiredArgsConstructor
открытый класс AgePredictionController {
закрытая конечная статическая строка API_ENDPOINT = "https://api.agify.io";
частный окончательный RestTemplate restTemplate;
- Пытается предсказать возраст для указанного имени.
- Если имя пусто, возвращается пустой прогноз.
- Имя @param, используемое для предсказания возраста
- Предсказание возраста @return для данного имени
@CrossOrigin(происхождение = "http://localhost:4200")
@GetMapping
public AgePrediction predictAge(@RequestParam(required = false) Строковое имя) {
если (StringUtils.isEmpty(имя)) {
вернуть AgePrediction.empty();
Заголовки HttpHeaders = new HttpHeaders();
headers.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
HttpEntity<?> entity = new HttpEntity<>(заголовки);
вернуть restTemplate.exchange (buildAgePredictionForNameURL (имя),
HttpMethod.GET, объект, AgePrediction.class).getBody();
частная строка buildAgePredictionForNameURL (имя строки) {
вернуть UriComponentsBuilder
.fromHttpUrl(API_ENDPOINT)
.queryParam("имя", имя)
.toUriString();
Создание интерфейса
Модель прогнозирования возраста будет определена как интерфейс с «именем» и «возрастом»:
предсказание возраста.модель.тс
```машинопись
интерфейс экспорта AgePredictionModel {
имя: строка;
возраст: число;
Веб-страница будет состоять из текста <input>
, где пользователи будут вводить имя, которое будет использоваться для предсказания возраста, и двух элементов <h3>
, где будут отображаться имя и предсказанный возраст.
Когда пользователи вводят <input>
, текст будет передан классу typescript через функцию onNameChanged($event)
.
Отображение name
и предполагаемого возраста
обрабатывается путем подписки на observable agePrediction$
.
app.component.html
```разметка
<дел>
<input id="nameInput"
тип = "текст"
(вход)="onNameChanged($event)"/>
<дел>
Имя: {{(agePrediction$ | async).name}}
<дел>
Возраст: {{(agePrediction$ | async).age}}
Что касается компонента Angular, он будет вызываться, когда происходят изменения в <input>
через функцию onNameChanged($event)
. Событие преобразуется в наблюдаемый объект с именем agePrediction$, который передается для запуска HTTP GET на серверную часть с самым последним именем. Это достигается за счет использования Subject nameSubject
и операторов RxJs debounceTime, DifferentUntilChanged, switchMap, shareReplay.
:::Информация
- debounceTime — выдает значение из источника Observable только после того, как прошел определенный промежуток времени без другого источника
- distinctUntilChanged - испускает все значения, переданные исходным наблюдаемым объектом, если они различны по сравнению с последним значением, испускаемым наблюдаемым результатом
- switchMap — проецирует каждое исходное значение в Observable, которое объединяется с выходным Observable, выдавая значения только из самого последнего спроецированного Observable.
- shareReplay - поделиться источником и воспроизвести указанное количество выпусков по подписке
app.component.ts
```машинопись
@Составная часть({
селектор: 'приложение-корень',
Url-шаблона: './app.component.html',
styleUrls: ['./app.component.css']
класс экспорта AppComponent реализует OnInit {
статический только для чтения AGE_PREDICTION_URL = 'http://localhost:8080/age/prediction';
agePrediction$: Observable
private nameSubject = new Subject
конструктор (частный http: HttpClient) {}
нгонинит () {
this.agePrediction$ = this.nameSubject.asObservable().pipe(
debounceTime(300),
отличный без изменений(),
switchMap(this.getAgePrediction),
поделитьсяПовторить()
- Получает модель прогнозирования возраста из нашего бэкенда Spring.
- Имя @param, используемое для предсказания возраста
getAgePrediction = (имя: строка): Observable
const params = new HttpParams().set('имя', имя);
вернуть this.http.get
{параметры});
onNameChanged($event) {
this.nameSubject.next($event.target.value);
Предварительный просмотр страницы предсказания возраста:
Написание интеграционного теста
В качестве первого шага тестирования веб-приложения создается абстрактный тестовый класс для инкапсуляции логики, необходимой для тестов Serenity:
- Актер представляет человека или систему, использующую тестируемое приложение — здесь просто называется «тестер».
- WebDriver — это интерфейс, используемый для управления веб-браузером. Указав аннотацию
@Managed
, Serenity внедрит экземпляр с конфигурацией по умолчанию вбраузер
- Внутри метода
setBaseUrl()
базовый URL-адрес, используемый для всех тестов, настраивается в EnvironmentVariables Serenity. Это сделано для того, чтобы избежать повторения протокола, хоста и порта для каждой тестовой страницы.
Абстрактинтегрионтест.java
```java
общедоступный абстрактный класс AbstractIntegrationTest {
@Удалось
защищенный браузер WebDriver;
защищенный тестер Актера;
частные EnvironmentVariablesvironmentVariables;
@BeforeEach
недействительным setUp () {
тестер = Актер.имя("Тестер");
tester.can(BrowseTheWeb.with(браузер));
установить базовый URL();
частная пустота setBaseUrl () {
environmentVariables.setProperty(WEBDRIVER_BASE_URL.getPropertyName(),
"http://локальный:4200");
Для тестирования страницы предсказания возраста создается новый класс IndexPage, наследуемый от PageObject (представление страницы в браузере). URL-адрес страницы относительно базового URL-адреса, указанного ранее, определяется с помощью аннотации @DefaultUrl
.
HTML-элементы, присутствующие на странице, легко определяются с помощью Serenity Screenplay.
IndexPage.java
```java
@DefaultUrl("/")
открытый класс IndexPage расширяет PageObject {
общедоступная статическая финальная цель NAME_INPUT =
the("ввод имени").located(By.id("nameInput"));
public static final Target PERSON_NAME =
the("текст заголовка имени").located(By.id("personName"));
общедоступная статическая финальная цель PERSON_AGE =
the("текст заголовка возраста").located(By.id("personAge"));
Наконец, написание интеграционного теста подразумевает класс, наследуемый от AbstractIntegrationTest, аннотированный с помощью @ExtendWith
JUnit и расширения JUnit 5 от Serenity. indexPage
будет введен Serenity во время выполнения теста. В стиле BDD тест структурирован в блоках «данное-когда-тогда».
Чтение того, что тест пытается достичь, почти так же просто, как чтение простого английского:
- Оператор «данный» попытается открыть браузер на странице предсказания возраста.
- Оператор ‘when’ получит дескриптор
<input>
и введет текст «Андрей».
- Оператор then будет оценивать 4 оператора:
- проверьте, видно ли имя человека
<h3>
на странице
- проверьте, является ли имя человека, отображаемое на странице, ожидаемым
- проверьте, отображается ли на странице возраст пользователя
<h3>
- проверьте, является ли возраст человека числом (не сверяясь с фиксированным возрастом, потому что прогноз возраста может измениться)
в конечном итоге
обеспечивает более медленный ответ серверной части, ожидая 5 секунд перед прохождением/непрохождением условия теста.
IndexPageTest.java
```java
@ExtendWith(SerenityJUnit5Extension.класс)
открытый класс IndexPageTest расширяет AbstractIntegrationTest {
private static final String TEST_NAME = "Андрей";
частная страница IndexPage indexPage;
@Контрольная работа
общественные недействительные данныеIndexPage_whenUserInputsName_thenAgePredictionIsDisplayedOnScreen () {
данныйЭто(тестер).wasAbleTo(Open.browserOn(indexPage));
когда(тестер).попытки(Введите.Значение(ИМЯ_ТЕСТА).в(ИМЯ_ВВОД));
затем (тестер). должен (
в конце концов(см.Это((ИМЯ_ЛИЦА),Видимо())),
в конце концов(см.Это((ИМЯ_ЛИЦА), содержитТекст(ИМЯ_ТЕСТА))),
в конце концов (см. Это ((PERSON_AGE), isVisible ())),
в конце концов (см. Это ((PERSON_AGE), isANNumber ()))
частный статический Predicate
return (htmlElement) -> htmlElement.getText().matches("\d*");
Резюме
В статье кратко показано, как можно использовать Serenity BDD для реализации интеграционных тестов для современного веб-приложения. Количество настроек, необходимых для выполнения тестов, сведено к минимуму, а получившийся код для тестирования веб-страниц так приятно читать, что заставляет задуматься, как он вообще работает!
:::Информация
Я не спонсирован и не получил никакой компенсации от каких-либо продуктов/услуг/компаний, перечисленных выше. Эта статья предназначена исключительно для информационных целей.
Рекомендации
Оригинал