Рубрики
Без рубрики

Тестовое поведение, а не реализация

Введение В прошлом году я потратил много времени на то, чтобы писать, а также практиковался в том, как писать… Помечено как тестирование, junit 5, mockito, java.

Вступление

В прошлом году я потратил много времени на написание, а также на практику написания хороших модульных/интеграционных тестов в AEM (Adobe Experience Manager). Теперь я хотел бы поделиться с вами тем, что я узнал до сих пор. То, что я узнал, связано не только с AEM, вы можете применить его к любому языку программирования или фреймворку.

До этого у меня был некоторый “опыт” в модульном тестировании. Я написал несколько модульных тестов, чтобы я мог сказать: “У меня есть опыт работы с этим”. Но, честно говоря, мне не нравилось это писать.

Несмотря на то, что я знал, какие преимущества тесты приносят продукту, который мы создаем, что приносит членам команды и мне, мне было все равно. Типичными оправданиями были:

  • ” У меня нет времени или у нас нет на это времени”
  • ” Я не могу проверить или высмеять это”
  • ” Вы не можете написать тест, выбрать какую-то новую функцию или исправить ошибку”

и в конце концов никто не заставлял меня писать это. К сожалению, написание тестов не было частью процесса разработки.

Теперь, когда я немного задумался, я не знал, как писать тесты. Давайте посмотрим правде в глаза, писать тесты непросто, как и многое другое, когда у вас нет опыта в этом.

К счастью, в какой-то момент все изменилось, и я хотел бы попытаться убедить всех вас, кто все еще думает как “старый” я.

Я бы хотел, чтобы мы все начали больше думать о качестве, а не о количестве.

Написание тестов как часть процесса разработки

Большинство из нас работает “гибким” способом (что бы это ни значило) и использует методологию DDD (разработка, ориентированная на крайние сроки) (я мог бы написать об этом в отдельном посте). Обычно это означает, что нет времени на написание тестов. Эта необходимость должна быть изменена, разработчики и все другие члены технической команды должны убедить всех остальных членов команды в том, что написание тестов должно быть частью процесса разработки. Написание тестов должно быть частью любой оценки. Период. Почему?

Есть много преимуществ, но я укажу на наиболее важные из них:

  1. Предотвращение ошибок
  2. Лучшее качество кода
  3. Предоставляет какую-то документацию
  4. Экономия времени
  5. Экономия денег
  6. Чувство “безопасности”

Теперь давайте посмотрим на типичные “недостатки”:

  1. Отнимающий много времени
  2. Отнимающий много денег
  3. Тесты пишутся медленно
  4. Тесты выполняются медленно
  5. Изменение реализации требует изменения тестов

Вероятно, вы заметили, что я упомянул время и деньги как преимущество и недостаток. В зависимости от того, думаете ли вы в краткосрочной перспективе, тогда да, это пустая трата времени и денег, но если вы думаете в долгосрочной перспективе, то это неправда. На самом деле, в конце концов, это экономит ваше время и деньги.

Многие люди думают, что у них мало времени и денег. Я думаю, потому что они не видят какого-то визуального результата от них, например, как они видят это, когда вы создаете какую-то функцию. Попробуйте мыслить так: с помощью тестов вы можете предотвратить множество ошибок и множество пинг-понгов между разработчиками и QAs. Очень часто у нас возникают запросы на изменение во время разработки, и случается, что мы реализовали что-то неправильно. Новый запрос просто больше не подходит к существующей реализации. Это означает, что нам нужно реорганизовать наш старый код или переопределить его с нуля. Здесь тесты дают вам некоторое ощущение безопасности, потому что вы знаете, нарушили вы поведение или нет. Другим примером могут быть большие, бесконечные проекты, в которых до вас работало несколько разных команд. Вероятно, в этом проекте плохо написана документация, вам нужно разобраться с устаревшим кодом и внедрить новые функции поверх него. Проведение тестов здесь – золото. Кроме того, многие проекты начинаются как MVP, который превращается в какой-то основной/базовый проект с несколькими подпроектами. Отсутствие тестового покрытия здесь – полная чушь.

Последние 3 недостатка также не соответствуют действительности.

  • Тесты пишутся медленно
    • да, Если вы не знаете, как это написать, и если у вас нет опыта
    • практика
  • Тесты выполняются медленно
    • и снова да, если вы не знаете, как это написать
  • Изменение реализации требует изменения тестов
    • да, потому что вы тестируете неправильные вещи
    • тестовое поведение не реализация

Ты мне не веришь?

Потратьте 1 час своего времени и посмотрите выступление “TDD, Где все пошло не так” от Яна Купера. Для меня это открыло глаза. До этого выступления я прочитал несколько книг о тестировании, и я не был так убежден. На мой взгляд, это определенно лучший разговор об этом.

tl ; доктор;

  • Поведение/требования к тестированию, а не реализация
    • при таком подходе вы устраните ранее упомянутые недостатки
  • Тестируйте общедоступный API модуля, а не классы, методы или технические детали
  • Модульный тест не должен быть сосредоточен на классах, методах, он должен быть сосредоточен на модуле, пользовательских историях
  • Тест дает вам обещание того, каким должен быть ожидаемый результат/поведение, поэтому, когда вы проводите рефакторинг реализации, используйте тесты, чтобы убедиться, что реализация по-прежнему дает ожидаемые результаты
  • Напишите тесты, чтобы охватить примеры использования или истории
  • Используйте модель “Дано, когда тогда”
  • Избегайте насмешек

Этот подход к тестированию поможет вам создать правильный продукт . Но отрицательным моментом может быть то, что это не поможет вам создать продукт справа . Другим недостатком является то, что вы не видите точно, что не так, когда тест терпит неудачу.

Таким образом, классический подход к модульному тестированию подталкивает вас к написанию более чистого и качественного кода, чем “тестирование поведения”. На мой взгляд, строгие проверки кода и инструменты статического анализа кода являются лучшим подходом для достижения того же результата. Второй недостаток для меня действительно незначителен, так как с помощью отладки вы можете быстро выяснить, что происходит.

Я надеюсь, что вы все еще следите за мной и что я начинаю немного менять ваше мышление о тестировании.

Теперь давайте остановимся на теории и посмотрим, как это работает на практике.

Тестирование в AEM

Поскольку последние несколько лет я работаю с AEM, я покажу вам, как тестировать поведение в ваших проектах AEM. То же самое вы можете применить к любым другим языкам программирования или фреймворкам. В зависимости от поддержки библиотеки тестирования это может быть проще или сложнее достичь.

В качестве примера предположим, что нам нужно реализовать API сведений о продукте, который используется на стороне клиента. Чтобы создать API сведений о продукте, допустим, весной вы, вероятно, создадите несколько классов, таких как Product Controller, Service, Repository, DTO и так далее. В игровом мире это означает, что вам необходимо создать сервлет Sling, службу OSGi, модель Sling и некоторые классы DTO.

Критерии приемлемости деталей продукта:

  • показать подробную информацию о продукте (идентификатор, название, описание, идентификатор категории, изображения и варианты)
  • варианты продуктов должны быть доступны для конкретной страны
  • варианты продукта доступны с определенной даты
  • варианты продукта требуют ‘ы должны быть отсортированы по порядку сортировки
  • название и описание продукта должны быть локализованы (в зависимости от рынка), отступление на английском языке

Реализация, которую вы увидите здесь, не идеальна, она упрощена и жестко запрограммирована. В реальном мире это сложнее. Но здесь реализация не важна, вместо этого мы должны сосредоточиться на том, как протестировать требования этого API.

Я добавлю сюда только 3 наиболее важных класса, другие реализации вы можете увидеть на Github

Подробная информация о продукте Sling Servlet

  • для обработки запроса
  • он выполняет некоторую проверку запроса
  • он использует сервис сведений о продукте, чтобы получить всю информацию о запрашиваемом продукте
package com.mkovacek.aem.core.servlets.products;

import com.mkovacek.aem.core.models.products.ProductDetailsModel;
import com.mkovacek.aem.core.records.response.Response;
import com.mkovacek.aem.core.services.products.ProductDetailsService;
import com.mkovacek.aem.core.services.response.ResponseService;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import javax.servlet.Servlet;
import javax.servlet.ServletException;

import java.io.IOException;

@Slf4j
@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = ProductDetailsServlet.RESOURCE_TYPE,
    selectors = ProductDetailsServlet.ALLOWED_SELECTOR,
    extensions = ProductDetailsServlet.JSON,
    methods = HttpConstants.METHOD_GET)
public class ProductDetailsServlet extends SlingSafeMethodsServlet {

    public static final String ALLOWED_SELECTOR = "productdetails";
    static final String RESOURCE_TYPE = "demo/components/productdetails";
    static final String JSON = "json";

    @Reference
    private transient ResponseService responseService;

    @Reference
    private transient ProductDetailsService productDetailsService;

    @Override
    public void doGet(final SlingHttpServletRequest request, final SlingHttpServletResponse response) throws ServletException, IOException {
        try {
            this.responseService.setJsonContentType(response);
            final String selector = request.getRequestPathInfo().getSelectorString();
            final String productId = this.responseService.getSuffix(request);

            if (this.responseService.areSelectorsValid(selector, ALLOWED_SELECTOR) && StringUtils.isNotBlank(productId)) {
                final Resource resource = request.getResource();
                final Response data = this.productDetailsService.getProductDetails(productId, resource);
                this.responseService.sendOk(response, data);
            } else {
                this.responseService.sendBadRequest(response);
            }
        } catch (final Exception e) {
            log.error("Exception during handling request", e);
            this.responseService.sendInternalServerError(response);
        }
    }

}

Подробная информация о продукте OSGi Service

  • это поиск запрошенного продукта в репозитории/базе данных
  • он проводит некоторую проверку продукта
  • сопоставляет ресурс продукта с моделью сведений о продукте
  • возвращает информацию о продукте
package com.mkovacek.aem.core.services.products.impl;

import com.day.cq.wcm.api.PageManager;
import com.mkovacek.aem.core.models.products.ProductDetailsModel;
import com.mkovacek.aem.core.records.response.Response;
import com.mkovacek.aem.core.records.response.Status;
import com.mkovacek.aem.core.services.products.ProductDetailsService;
import com.mkovacek.aem.core.services.resourceresolver.ResourceResolverService;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

import java.util.Locale;
import java.util.Optional;

@Slf4j
@Component(service = ProductDetailsService.class, immediate = true)
public class ProductDetailsServiceImpl implements ProductDetailsService {

    private static final String PIM_READER = "pimReader";
    private static final Response notFoundResponse = new Response<>(new Status(true, "Product Details not found"), null);
    private static final Response errorResponse = new Response<>(new Status(false, "Error during fetching product details"), null);

    @Reference
    private ResourceResolverService resourceResolverService;

    @Override
    public Response getProductDetails(final String id, final Resource resource) {
        try (final ResourceResolver resourceResolver = this.resourceResolverService.getResourceResolver(PIM_READER)) {
            final Locale locale = resourceResolver.adaptTo(PageManager.class).getContainingPage(resource).getLanguage(false);
            //usually this would be implemented with query
            final String productPath = StringUtils.join("/var/commerce/products/demo/", id);
            return Optional.ofNullable(resourceResolver.getResource(productPath))
                       .map(productResource -> productResource.adaptTo(ProductDetailsModel.class))
                       .map(productDetailsModel -> productDetailsModel.setLocale(locale))
                       .filter(ProductDetailsModel::isValid)
                       .map(productDetailsModel -> new Response<>(new Status(true), productDetailsModel))
                       .orElse(notFoundResponse);
        } catch (final Exception e) {
            log.error("Exception during fetching product details", e);
        }
        return errorResponse;
    }

}

Подробная информация о продукте Модель слинга

  • представление ресурса продукта в репозитории/базе данных
  • используется в качестве ответа в формате JSON
package com.mkovacek.aem.core.models.products;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.mkovacek.aem.core.services.products.ProductLocalizationService;
import com.mkovacek.aem.core.services.products.ProductValidatorService;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ChildResource;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;

@Slf4j
@Model(adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class ProductDetailsModel {

    @ValueMapValue
    @Default(values = StringUtils.EMPTY)
    @Getter
    private String id;

    @ValueMapValue
    @Default(values = StringUtils.EMPTY)
    @Getter
    private String categoryId;

    @ChildResource
    @Getter
    private List images;

    @ChildResource
    private List variants;

    @Self
    private ValueMap valueMap;

    @OSGiService
    private ProductLocalizationService productLocalizationService;

    @OSGiService
    private ProductValidatorService productValidatorService;

    @Getter
    @JsonProperty("variants")
    private List validVariants = new ArrayList<>();

    @Getter
    private String name = StringUtils.EMPTY;

    @Getter
    private String description = StringUtils.EMPTY;

    @JsonIgnore
    public boolean isValid() {
        return !this.validVariants.isEmpty();
    }

    @JsonIgnore
    public ProductDetailsModel setLocale(final Locale locale) {
        this.setLocalizedValues(locale);
        this.validateAndSortVariants(locale);
        return this;
    }

    private void setLocalizedValues(final Locale locale) {
        this.name = this.productLocalizationService.getLocalizedProductDetail(this.valueMap, "name.", locale);
        this.description = this.productLocalizationService.getLocalizedProductDetail(this.valueMap, "description.", locale);
    }

    private void validateAndSortVariants(final Locale locale) {
        this.validVariants = this.productValidatorService.getValidVariants(this.variants, locale);
        this.validVariants.sort(Comparator.comparing(VariantsModel::getSortOrder));
    }

}

Кроме этих 3 классов, мне нужно создать еще несколько:

Вы видели, что у нас есть много классов для создания этой пользовательской истории. Обычно то, что разработчик тестировал бы здесь, – это сервисы OSGi. Я не говорю, что это плохой подход, но для этого вам потребуется больше времени, и каждый раз, когда вы будете рефакторировать свой код или добавлять что-то новое, очень вероятно, что вам также потребуется изменить свои тесты.

Вместо этого давайте протестируем только сервлет, потому что это общедоступный API этой пользовательской истории. Итак, что нам нужно протестировать в сервлете? Прежде всего, нам нужно охватить все требования из критериев приемлемости, дополнительно мы можем осветить некоторые технические детали реализации сервлета.

Тестовые библиотеки в AEM

На данный момент, на мой взгляд, лучшая библиотека, которую вы можете использовать, – это AEM Mocks . AEM Mocks поддерживает наиболее распространенные макетные реализации AEM API + содержит макетные реализации Apache Sling и OSGi. Для других не реализованных макетов вам нужно будет реализовать его самостоятельно или использовать Мокито . Помимо этих двух, я буду использовать Junit 5.

Несколько советов, прежде чем мы начнем:

  • Постарайтесь, чтобы тестовые классы были как можно более чистыми, они должны содержать только тесты.
  • Переместить макеты в отдельные классы
  • Создайте несколько классов Util с общими вспомогательными методами, если вы повторяетесь в нескольких местах
  • Используйте @beforeAll/afterall, @beforeEach/afterEach, аннотации Junit 5, чтобы не повторяться в каждом методе тестирования и ускорить ваши тесты
  • Создайте общий контекст AEM в отдельном классе, если вы повторяетесь в нескольких тестовых классах
  • Не создавайте программно сложные ресурсы в контексте AEM, вместо этого экспортируйте их из реального экземпляра AEM в качестве ресурса JSON и загружайте в контекст AEM.
  • Используйте макет ResourceResolver всякий раз, когда это возможно, чтобы ускорить ваши тесты

Подробная информация о продукте ServletTest

Вы увидите, что этот тестовый класс более или менее чистый и ориентирован только на тесты. Здесь нет насмешки, разделенный пример макета вы можете увидеть здесь . Я использую @beforeAll и @beforeEach для выполнения некоторых общих настроек, таких как настройка маркетинговых страниц/ресурсов и общей информации о запросах. Также мне нужен был какой-нибудь вспомогательный класс, чтобы легче регистрировать все необходимые классы в AEM context . Все ресурсы экспортируются в формате JSON из реального экземпляра AEM и импортируются в контекст AEM, чтобы мы тестировали на реальных данных .

В этом тестовом классе я тестирую технические детали и требования

  • технические детали
    • проверка запроса
  • требования
    • ответ для несуществующего продукта i d
    • подробная информация о продукте на разных рынках для покрытия локализации
    • проверка вариантов продукта для конкретных рынков
    • доступность вариантов продукта с определенной даты
    • сортировка вариантов продукта
package com.mkovacek.aem.core.servlets.products;

import com.day.cq.wcm.api.Page;
import com.mkovacek.aem.core.context.AppAemContextBuilder;
import com.mkovacek.aem.core.context.constants.TestConstants;
import com.mkovacek.aem.core.context.utils.ResourceUtil;
import com.mkovacek.aem.core.services.blobstorage.impl.BlobStorageServiceImpl;
import com.mkovacek.aem.core.services.products.impl.ProductDetailsServiceImpl;
import com.mkovacek.aem.core.services.products.impl.ProductLocalizationServiceImpl;
import com.mkovacek.aem.core.services.products.impl.ProductValidatorServiceImpl;
import com.mkovacek.aem.core.services.resourceresolver.impl.ResourceResolverServiceImpl;
import com.mkovacek.aem.core.services.response.impl.ResponseServiceImpl;

import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.testing.mock.sling.servlet.MockRequestPathInfo;
import org.apache.sling.testing.resourceresolver.MockResourceResolverFactory;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Collections;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(AemContextExtension.class)
class ProductDetailsServletTest {

    private static final AemContext context = new AppAemContextBuilder()
                                                  .loadResource(TestConstants.HR_HR_LANDING_PAGE_JSON, TestConstants.HR_HR_LANDING_PAGE_PATH)
                                                  .loadResource(TestConstants.DE_AT_LANDING_PAGE_JSON, TestConstants.DE_AT_LANDING_PAGE_PATH)
                                                  .loadResource(TestConstants.FR_FR_LANDING_PAGE_JSON, TestConstants.FR_FR_LANDING_PAGE_PATH)
                                                  .loadResource(TestConstants.PRODUCTS_JSON, TestConstants.PRODUCTS_PATH)
                                                  .registerService(ResourceResolverFactory.class, new MockResourceResolverFactory())
                                                  .registerInjectActivateService(new ResourceResolverServiceImpl())
                                                  .registerInjectActivateService(new ResponseServiceImpl())
                                                  .registerInjectActivateService(new BlobStorageServiceImpl(), Collections.singletonMap("productImagesFolderPath", "https://dummyurl.com/images/products/"))
                                                  .registerInjectActivateService(new ProductValidatorServiceImpl())
                                                  .registerInjectActivateService(new ProductLocalizationServiceImpl())
                                                  .registerInjectActivateService(new ResponseServiceImpl())
                                                  .registerInjectActivateService(new ProductDetailsServiceImpl())
                                                  .build();

    private static final MockRequestPathInfo requestPathInfo = context.requestPathInfo();
    private final ProductDetailsServlet servlet = context.registerInjectActivateService(new ProductDetailsServlet());
    private static final String CONTENT_RESOURCE_PATH = "root/productdetails";
    private static String NOT_FOUND_RESPONSE;
    private static String BAD_REQUEST_RESPONSE;

    @BeforeAll
    static void setUpBeforeAllTests() throws IOException {
        context.addModelsForPackage(TestConstants.SLING_MODELS_PACKAGES);
        requestPathInfo.setExtension("json");
        NOT_FOUND_RESPONSE = ResourceUtil.getExpectedResult(ProductDetailsServlet.class, "responses/not-found-response.json");
        BAD_REQUEST_RESPONSE = ResourceUtil.getExpectedResult(ProductDetailsServlet.class, "responses/bad-request-response.json");
    }

    @BeforeEach
    void setupBeforeEachTest() {
        context.response().resetBuffer();
        requestPathInfo.setSelectorString(ProductDetailsServlet.ALLOWED_SELECTOR);
        requestPathInfo.setSuffix("123456789");
        final Page page = context.pageManager().getPage(TestConstants.HR_HR_LANDING_PAGE_PATH);
        context.request().setResource(page.getContentResource(CONTENT_RESOURCE_PATH));
    }

    @Test
    @DisplayName("GIVEN landing page (en-HR) WHEN servlet is called with not valid selector THEN it returns bad request response in JSON format")
    void testNotValidSelector() throws ServletException, IOException {
        requestPathInfo.setSelectorString(ProductDetailsServlet.ALLOWED_SELECTOR + ".test");
        this.servlet.doGet(context.request(), context.response());

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()),
            () -> assertEquals(BAD_REQUEST_RESPONSE, context.response().getOutputAsString())
        );
    }

    @Test
    @DisplayName("GIVEN landing page (en-HR) WHEN servlet is called without productId suffix THEN it returns bad request response in JSON format")
    void testNoProductId() throws ServletException, IOException {
        requestPathInfo.setSuffix(StringUtils.EMPTY);
        this.servlet.doGet(context.request(), context.response());

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()),
            () -> assertEquals(BAD_REQUEST_RESPONSE, context.response().getOutputAsString())
        );
    }

    @Test
    @DisplayName("GIVEN landing page (en-HR) WHEN servlet is called with not existing productId THEN it returns not found response in JSON format")
    void testNotExistingProductId() throws ServletException, IOException {
        requestPathInfo.setSuffix("123abc");
        this.servlet.doGet(context.request(), context.response());

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_OK, context.response().getStatus()),
            () -> assertEquals(NOT_FOUND_RESPONSE, context.response().getOutputAsString())
        );
    }

    @Test
    @DisplayName("GIVEN landing page (en-HR) WHEN servlet is called with existing productId THEN it returns an expected localized (fallback) product details response in JSON format")
    void testProductDetailsInCroatianMarket() throws ServletException, IOException {
        this.servlet.doGet(context.request(), context.response());
        final String expectedProductDetails = ResourceUtil.getExpectedResult(this.getClass(), "responses/product-123456789-hr-HR.json");

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_OK, context.response().getStatus()),
            () -> assertEquals(expectedProductDetails, context.response().getOutputAsString())
        );
    }

    @Test
    @DisplayName("GIVEN landing page (de-AT) WHEN servlet is called with existing productId THEN it returns an expected localized product details response in JSON format")
    void testProductDetailsInAustrianMarket() throws ServletException, IOException {
        this.setPageResource(TestConstants.DE_AT_LANDING_PAGE_PATH);
        this.servlet.doGet(context.request(), context.response());
        final String expectedProductDetails = ResourceUtil.getExpectedResult(this.getClass(), "responses/product-123456789-at-DE.json");

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_OK, context.response().getStatus()),
            () -> assertEquals(expectedProductDetails, context.response().getOutputAsString())
        );
    }

    @Test
    @DisplayName("GIVEN landing page (fr-FR) WHEN servlet is called with existing productId which is not valid for French market THEN it returns not found response in JSON format")
    void testProductDetailsInFrenchMarket() throws ServletException, IOException {
        this.setPageResource(TestConstants.FR_FR_LANDING_PAGE_PATH);
        this.servlet.doGet(context.request(), context.response());

        assertAll(
            () -> assertEquals(HttpServletResponse.SC_OK, context.response().getStatus()),
            () -> assertEquals(NOT_FOUND_RESPONSE, context.response().getOutputAsString())
        );
    }

    private void setPageResource(final String path) {
        final Page page = context.pageManager().getPage(path);
        context.request().setResource(page.getContentResource(CONTENT_RESOURCE_PATH));
    }
}

С помощью этого подхода к тестированию я охватил 87% строк кода. Остальные 13% того, что не охвачено, – это улавливание исключений.

Другими хорошими примерами для тестирования в AEM могут быть компоненты. Для каждого компонента у вас есть требования. Для достижения этих требований вы, вероятно, создадите несколько классов, таких как OSGi service, некоторые утилиты, записи и те требования, которые вы будете публично раскрывать через Sling model для просмотра layer. Идеальные кандидаты для тестирования.

Подводить итоги

  • Если вы не пишете тесты, начните писать их
  • Требования к тестированию, а не реализация
  • У разработчиков должно быть время для написания тестов

Оригинал: “https://dev.to/mkovacek/test-behaviour-not-implementation-3g2j”