Angular и Spring Boot — использование Serenity BDD для интеграционного тестирования

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.


:::Информация






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),


поделитьсяПовторить()




getAgePrediction = (имя: строка): Observable => {


const params = new HttpParams().set('имя', имя);


вернуть this.http.get(AppComponent.AGE_PREDICTION_URL,


{параметры});


onNameChanged($event) {


this.nameSubject.next($event.target.value);


Предварительный просмотр страницы предсказания возраста:


предварительный просмотр страницы предсказания возраста


Написание интеграционного теста


В качестве первого шага тестирования веб-приложения создается абстрактный тестовый класс для инкапсуляции логики, необходимой для тестов 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 тест структурирован в блоках «данное-когда-тогда».


Чтение того, что тест пытается достичь, почти так же просто, как чтение простого английского:









в конечном итоге обеспечивает более медленный ответ серверной части, ожидая 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 isANumber() {


return (htmlElement) -> htmlElement.getText().matches("\d*");


Резюме


В статье кратко показано, как можно использовать Serenity BDD для реализации интеграционных тестов для современного веб-приложения. Количество настроек, необходимых для выполнения тестов, сведено к минимуму, а получившийся код для тестирования веб-страниц так приятно читать, что заставляет задуматься, как он вообще работает!


:::Информация


Я не спонсирован и не получил никакой компенсации от каких-либо продуктов/услуг/компаний, перечисленных выше. Эта статья предназначена исключительно для информационных целей.


Рекомендации








Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE