Вступление
В прошлом году я потратил много времени на написание, а также на практику написания хороших модульных/интеграционных тестов в AEM (Adobe Experience Manager). Теперь я хотел бы поделиться с вами тем, что я узнал до сих пор. То, что я узнал, связано не только с AEM, вы можете применить его к любому языку программирования или фреймворку.
До этого у меня был некоторый “опыт” в модульном тестировании. Я написал несколько модульных тестов, чтобы я мог сказать: “У меня есть опыт работы с этим”. Но, честно говоря, мне не нравилось это писать.
Несмотря на то, что я знал, какие преимущества тесты приносят продукту, который мы создаем, что приносит членам команды и мне, мне было все равно. Типичными оправданиями были:
- ” У меня нет времени или у нас нет на это времени”
- ” Я не могу проверить или высмеять это”
- ” Вы не можете написать тест, выбрать какую-то новую функцию или исправить ошибку”
и в конце концов никто не заставлял меня писать это. К сожалению, написание тестов не было частью процесса разработки.
Теперь, когда я немного задумался, я не знал, как писать тесты. Давайте посмотрим правде в глаза, писать тесты непросто, как и многое другое, когда у вас нет опыта в этом.
К счастью, в какой-то момент все изменилось, и я хотел бы попытаться убедить всех вас, кто все еще думает как “старый” я.
Я бы хотел, чтобы мы все начали больше думать о качестве, а не о количестве.
Написание тестов как часть процесса разработки
Большинство из нас работает “гибким” способом (что бы это ни значило) и использует методологию DDD (разработка, ориентированная на крайние сроки) (я мог бы написать об этом в отдельном посте). Обычно это означает, что нет времени на написание тестов. Эта необходимость должна быть изменена, разработчики и все другие члены технической команды должны убедить всех остальных членов команды в том, что написание тестов должно быть частью процесса разработки. Написание тестов должно быть частью любой оценки. Период. Почему?
Есть много преимуществ, но я укажу на наиболее важные из них:
- Предотвращение ошибок
- Лучшее качество кода
- Предоставляет какую-то документацию
- Экономия времени
- Экономия денег
- Чувство “безопасности”
Теперь давайте посмотрим на типичные “недостатки”:
- Отнимающий много времени
- Отнимающий много денег
- Тесты пишутся медленно
- Тесты выполняются медленно
- Изменение реализации требует изменения тестов
Вероятно, вы заметили, что я упомянул время и деньги как преимущество и недостаток. В зависимости от того, думаете ли вы в краткосрочной перспективе, тогда да, это пустая трата времени и денег, но если вы думаете в долгосрочной перспективе, то это неправда. На самом деле, в конце концов, это экономит ваше время и деньги.
Многие люди думают, что у них мало времени и денег. Я думаю, потому что они не видят какого-то визуального результата от них, например, как они видят это, когда вы создаете какую-то функцию. Попробуйте мыслить так: с помощью тестов вы можете предотвратить множество ошибок и множество пинг-понгов между разработчиками и 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 Responsedata = 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 ResponsenotFoundResponse = 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 Listimages; @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 классов, мне нужно создать еще несколько:
- Модель изображения , Варианты Модели
- Служба хранения больших двоичных объектов , ProductValidatorService , Услуга локализации продукта , Служба распознавания ресурсов , Служба реагирования
- Ответ и Статус записи
Вы видели, что у нас есть много классов для создания этой пользовательской истории. Обычно то, что разработчик тестировал бы здесь, – это сервисы 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”