本教程介绍单元测试的实施,该单元测试将验证在
自定义组件
教程中创建的Byline组件的Sling模型的行为。
先决条件
prerequisites
查看设置
本地开发环境
所需的工具和说明。
如果系统上同时安装了Java™ 8和Java™ 11,则VS代码测试运行程序在执行测试时可能会选择较低的Java™运行时,从而导致测试失败。 如果发生这种情况,请卸载Java™ 8.
如果成功完成了上一章,则可以重用该项目并跳过签出入门项目的步骤。
查看本教程所基于的基本行代码:
背景
unit-testing-background
在本教程中,我们将了解如何为署名组件的
Sling模型
(在
创建自定义AEM组件
中创建)编写
单元测试
。 单元测试是用Java™编写的构建时测试,用于验证Java™代码的预期行为。 每个单元测试通常很小,可根据预期结果验证方法(或工作单元)的输出。
我们采用AEM最佳实践,并采用:
单元测试和AdobeCloud Manager
unit-testing-and-adobe-cloud-manager
AdobeCloud Manager
将单元测试执行和
代码覆盖率报告
集成到其CI/CD管道中,以帮助鼓励和促进单元测试AEM代码的最佳实践。
虽然单元测试代码是任何代码库的一个良好实践,但在使用Cloud Manager时,通过为Cloud Manager提供单元测试来利用其代码质量测试和报告工具非常重要。
更新测试Maven依赖项
inspect-the-test-maven-dependencies
第一步是检查Maven依赖项以支持编写和运行测试。 需要四个依赖项:
要查看这些依赖项,请打开位于
aem-guides-wknd/pom.xml
的父Reactor POM,导航到
<dependencies>..</dependencies>
并查看
<!-- Testing -->
下io.wcm的JUnit、Mockito、Apache Sling Mocks和AEM Mock Tests的依赖项。
确保
io.wcm.testing.aem-mock.junit5
设置为
4.1.0
:
code language-xml
<groupId>io.wcm</groupId>
<artifactId>io.wcm.testing.aem-mock.junit5</artifactId>
<version>4.1.0</version>
<scope>test</scope>
</dependency>
src/main/java/com/adobe/aem/guides/wknd/core/models/impl/BylineImpl.java
在上创建相应的单元测试Java™类
src/test/java/com/adobe/aem/guides/wknd/core/models/impl/BylineImplTest.java
单元测试文件
BylineImplTest.java
上的
Test
后缀是一个约定,允许我们
轻松将其标识为
BylineImpl.java
的测试文件
但是,还要区分测试文件
和
所测试的类
BylineImpl.java
package com.adobe.aem.guides.wknd.core.models.impl;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class BylineImplTest {
@BeforeEach
void setUp() throws Exception {
@Test
void testGetName() {
fail("Not yet implemented");
@Test
void testGetOccupations() {
fail("Not yet implemented");
@Test
void testIsEmpty() {
fail("Not yet implemented");
第一个方法public void setUp() { .. }
使用JUnit的@BeforeEach
进行注释,它指示JUnit测试运行程序在运行该类中的每个测试方法之前执行此方法。 这为初始化所有测试所需的通用测试状态提供了一个方便的位置。
后续方法是测试方法,其名称按惯例以test
为前缀,并以@Test
注释标记。 请注意,默认情况下,我们的所有测试都设置为失败,因为尚未实施它们。
首先,我们先对我们测试的类中的每个公共方法使用一个测试方法,因此:
table 0-row-3 1-row-3 2-row-3 3-row-3
在本教程中,将使用后一种方法(因为我们在上一章中创建了有效的 BylineImpl.java)。 因此,我们必须回顾和了解其公共方法的行为,同时也要了解其实施细节。 这听起来可能恰恰相反,因为一个良好的测试应该只关注输入和输出,然而在AEM中工作时,为了构建工作测试,需要理解各种实施注意事项。
在AEM的上下文中,TDD需要一定程度的专业知识,并且最适合于精通AEM代码的AEM开发和单元测试的AEM开发人员。
设置AEM测试上下文 setting-up-aem-test-context
大多数为AEM编写的代码依赖于JCR、Sling或AEM API,这反过来又需要运行的AEM的上下文才能正确执行。
由于单元测试是在构建时执行的,因此在正在运行的AEM实例的上下文之外,没有此类上下文。 为此,wcm.io的AEM Mocks创建了模拟上下文,使这些API 大部分 可以像在AEM中运行一样。
import org.junit.jupiter.api.extension.ExtendWith;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
@ExtendWith(AemContextExtension.class)
class BylineImplTest {
private final AemContext ctx = new AemContext();
在此上下文中创建模拟JCR内容结构
可在此上下文中注册自定义OSGi服务
提供各种常见的必需模拟对象和帮助程序,如SlingHttpServletRequest对象;各种模拟Sling和AEM OSGi服务,如ModelFactory、PageManager、Page、Template、ComponentManager、Component、TagManager、Tag等。
并非这些对象的所有方法都已实现!
以及更多!
ctx
对象将作为大多数模拟上下文的入口点。
在每个@Test
方法之前执行的setUp(..)
方法中,定义一个常见的模拟测试状态:
code language-java
@BeforeEach
public void setUp() throws Exception {
ctx.addModelsForClasses(BylineImpl.class);
ctx.load().json("/com/adobe/aem/guides/wknd/core/models/impl/BylineImplTest.json", "/content");
addModelsForClasses
将要测试的Sling模型注册到模拟AEM Context中,以便可在@Test
方法中实例化。
load().json
将资源结构加载到模拟上下文中,允许代码与这些资源交互,就像它们由真实存储库提供一样。 文件 BylineImplTest.json
中的资源定义已加载到 /content 下的模拟JCR上下文中。
BylineImplTest.json
尚不存在,因此让我们创建它并定义测试所需的JCR资源结构。
表示模拟资源结构的JSON文件存储在 core/src/test/resources 下,遵循与JUnit Java™测试文件相同的包路径。
在core/test/resources/com/adobe/aem/guides/wknd/core/models/impl
处创建名为 BylineImplTest.json 的JSON文件,该文件包含以下内容:
code language-json
"byline": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline"
此JSON为Byline组件单元测试定义了一个模拟资源(JCR节点)。 此时,JSON具有表示Byline组件内容资源所需的最小属性集jcr:primaryType
和sling:resourceType
。
处理单元测试时的一般规则是创建满足每个测试所需的最小模拟内容、上下文和代码集。 避免在编写测试之前构建完整的模拟上下文的诱惑,因为这通常会导致不需要的工件。
现在存在 BylineImplTest.json,执行ctx.json("/com/adobe/aem/guides/wknd/core/models/impl/BylineImplTest.json", "/content")
时,模拟资源定义将加载到路径 /content. 处的上下文中
ctx.currentResource("/content/byline");
Byline byline = ctx.request().adaptTo(Byline.class);
String actual = byline.getName();
assertEquals(expected, actual);
String expected
设置预期值。 我们将此项设置为"Jane Done"。
ctx.currentResource
设置模拟资源的上下文以评估代码,因此设置为 /content/byline,因为这是加载模拟署名内容资源的位置。
Byline byline
通过从模拟请求对象中调整来实例化署名Sling模型。
String actual
在Byline Sling模型对象上调用我们正在测试的方法getName()
。
assertEquals
声明预期值与署名Sling模型对象返回的值匹配。 如果这些值不相等,测试将失败。
运行测试……但测试失败,出现NullPointerException
。
此测试不会失败,因为我们从未在模拟JSON中定义name
属性,这将导致测试失败,但测试执行尚未到达该点! 此测试失败,因为署名对象本身存在NullPointerException
。
在BylineImpl.java
中,如果@PostConstruct init()
引发异常,则会阻止Sling模型实例化,并导致该Sling模型对象为空。
code language-java
@PostConstruct
private void init() {
image = modelFactory.getModelFromWrappedRequest(request, request.getResource(), Image.class);
结果发现,虽然ModelFactory OSGi服务是通过AemContext
(通过Apache Sling Context)提供的,但并非所有方法都已实现,包括在BylineImpl的init()
方法中调用的getModelFromWrappedRequest(...)
。 这会导致AbstractMethodError,它导致init()
失败,因此ctx.request().adaptTo(Byline.class)
的自适应结果为null对象。
由于提供的模拟无法容纳我们的代码,因此我们必须自己实现模拟上下文。为此,我们可以使用Mockito创建模拟ModelFactory对象,当对其调用getModelFromWrappedRequest(...)
时,它会返回模拟Image对象。
由于甚至要实例化署名Sling模型,此模拟上下文必须就位,因此我们可以将其添加到@Before setUp()
方法。 我们还需要将MockitoExtension.class
添加到 BylineImplTest 类上方的@ExtendWith
批注。
code language-java
package com.adobe.aem.guides.wknd.core.models.impl;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.Mock;
import com.adobe.aem.guides.wknd.core.models.Byline;
import com.adobe.cq.wcm.core.components.models.Image;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.sling.models.factory.ModelFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.apache.sling.api.resource.Resource;
@ExtendWith({ AemContextExtension.class, MockitoExtension.class })
public class BylineImplTest {
private final AemContext ctx = new AemContext();
@Mock
private Image image;
@Mock
private ModelFactory modelFactory;
@BeforeEach
public void setUp() throws Exception {
ctx.addModelsForClasses(BylineImpl.class);
ctx.load().json("/com/adobe/aem/guides/wknd/core/models/impl/BylineImplTest.json", "/content");
lenient().when(modelFactory.getModelFromWrappedRequest(eq(ctx.request()), any(Resource.class), eq(Image.class)))
.thenReturn(image);
ctx.registerService(ModelFactory.class, modelFactory, org.osgi.framework.Constants.SERVICE_RANKING,
Integer.MAX_VALUE);
@Test
void testGetName() { ...
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
将测试用例类标记为使用Mockito JUnit Jupiter扩展运行,该扩展允许使用@Mock注释在类级别定义模拟对象。
@Mock private Image
创建类型为com.adobe.cq.wcm.core.components.models.Image
的模拟对象。 这是在类级别上定义的,这样@Test
方法就可以根据需要更改其行为。
@Mock private ModelFactory
创建ModelFactory类型的模拟对象。 这是一个纯粹的Mockito模拟,没有实现任何方法。 这是在类级别上定义的,这样@Test
方法就可以根据需要更改其行为。
when(modelFactory.getModelFromWrappedRequest(..)
在模拟ModelFactory对象上调用getModelFromWrappedRequest(..)
时为其注册模拟行为。 在thenReturn (..)
中定义的结果是返回模拟图像对象。 仅在以下情况下调用此行为:第一个参数等于ctx
的请求对象,第二个参数是任何Resource对象,第三个参数必须是核心组件Image类。 我们接受任何资源,因为在整个测试中,我们将ctx.currentResource(...)
设置为 BylineImplTest.json 中定义的各种模拟资源。 请注意,我们添加了 lenient() 严格性,因为稍后我们将要覆盖ModelFactory的此行为。
ctx.registerService(..)
。 将模拟ModelFactory对象注册到AemContext中,具有最高的服务等级。 这是必需的,因为BylineImpl的init()
中使用的ModelFactory是通过@OSGiService ModelFactory model
字段注入的。 为了使AemContext注入 我们的 模拟对象(该对象处理对getModelFromWrappedRequest(..)
的调用),我们必须将其注册为该类型的最高级别服务(ModelFactory)。
重新运行测试,再次失败,但这次消息清楚地表明了失败的原因。
由于断言 , testGetName()失败
我们收到一个 AssertionError,表示测试中的断言条件失败,它告诉我们 预期值为“Jane Doe”,但 实际值为null。 这是有道理的,因为“name” 属性尚未添加到 BylineImplTest.json 中的模拟 /content/byline 资源定义中,因此让我们添加它:
更新 BylineImplTest.json 以定义"name": "Jane Doe".
code language-json
"byline": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"name": "Jane Doe"
测试getOccupations() testing-get-occupations
太好了! 第一个测试已经通过! 让我们继续并测试getOccupations()
。 由于模拟上下文的初始化已在@Before setUp()
方法中完成,因此该测试用例中的所有@Test
方法均可使用此方法,包括getOccupations()
。
请记住,此方法必须返回按字母顺序排序的占有列表(降序)存储在occutions属性中。
@Test
public void testGetOccupations() {
List<String> expected = new ImmutableList.Builder<String>()
.add("Blogger")
.add("Photographer")
.add("YouTuber")
.build();
ctx.currentResource("/content/byline");
Byline byline = ctx.request().adaptTo(Byline.class);
List<String> actual = byline.getOccupations();
assertEquals(expected, actual);
List<String> expected
定义预期的结果。
ctx.currentResource
将当前资源设置为根据/content/byline处的模拟资源定义评估上下文。 这可确保在模拟资源的上下文中执行 BylineImpl.java。
ctx.request().adaptTo(Byline.class)
通过从模拟请求对象中调整来实例化署名Sling模型。
byline.getOccupations()
在Byline Sling模型对象上调用我们正在测试的方法getOccupations()
。
assertEquals(expected, actual)
声明预期列表与实际列表相同。
请记住,与上述 getName()
一样,BylineImplTest.json 未定义占用,因此如果运行该测试,测试将失败,因为byline.getOccupations()
将返回空列表。
更新 BylineImplTest.json 以包含职业列表,这些职业按非字母顺序设置,以确保我们的测试验证职业是否按 getOccupations()
的字母顺序排序。
code language-json
"byline": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"name": "Jane Doe",
"occupations": ["Photographer", "Blogger", "YouTuber"]
"byline": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"name": "Jane Doe",
"occupations": ["Photographer", "Blogger", "YouTuber"]
"empty": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline"
"empty": {...}
定义名为“empty”的新资源定义,该定义只具有jcr:primaryType
和sling:resourceType
。
请记住,我们在@setUp
中执行每个测试方法之前将BylineImplTest.json
加载到ctx
中,因此我们在 /content/empty. 的测试中立即可以使用此新资源定义。
按如下方式更新testIsEmpty()
,将当前资源设置为新的"empty"模拟资源定义。
code language-java
public void testIsEmpty() {
ctx.currentResource("/content/empty");
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
"byline": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"name": "Jane Doe",
"occupations": ["Photographer", "Blogger", "YouTuber"]
"empty": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline"
"without-name": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"occupations": "[Photographer, Blogger, YouTuber]"
"without-occupations": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "wknd/components/content/byline",
"name": "Jane Doe"
ctx.currentResource("/content/empty");
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
@Test
public void testIsEmpty_WithoutName() {
ctx.currentResource("/content/without-name");
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
@Test
public void testIsEmpty_WithoutOccupations() {
ctx.currentResource("/content/without-occupations");
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
@Test
public void testIsEmpty_WithoutImage() {
ctx.currentResource("/content/byline");
lenient().when(modelFactory.getModelFromWrappedRequest(eq(ctx.request()),
any(Resource.class),
eq(Image.class))).thenReturn(null);
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
@Test
public void testIsEmpty_WithoutImageSrc() {
ctx.currentResource("/content/byline");
when(image.getSrc()).thenReturn("");
Byline byline = ctx.request().adaptTo(Byline.class);
assertTrue(byline.isEmpty());
testIsEmpty()
针对空模拟资源定义进行测试,并断言isEmpty()
为true。
针对具有占用但没有名称的模拟资源定义进行 testIsEmpty_WithoutName()
测试。
针对名称为但无占用空间的模拟资源定义进行 testIsEmpty_WithoutOccupations()
测试。
testIsEmpty_WithoutImage()
针对具有名称和占用情况的模拟资源定义进行测试,但将模拟图像设置为返回空值。 请注意,我们要覆盖setUp()
中定义的modelFactory.getModelFromWrappedRequest(..)
行为,以确保此调用返回的图像对象为null。 Mockito桩模块功能非常严格,并且不需要重复的代码。 因此,我们将模拟设置为 lenient
设置,以明确说明我们正在覆盖setUp()
方法中的行为。
testIsEmpty_WithoutImageSrc()
针对具有名称和占用情况的模拟资源定义进行测试,但设置模拟图像以在调用getSrc()
时返回空白字符串。
最后,编写测试以确保 isEmpty() 在正确配置组件时返回false。 对于这种情况,我们可以重用 /content/byline,它表示完全配置的Byline组件。
code language-java
public void testIsNotEmpty() {
ctx.currentResource("/content/byline");
when(image.getSrc()).thenReturn("/content/bio.png");
Byline byline = ctx.request().adaptTo(Byline.class);
assertFalse(byline.isEmpty());