The talk presents how we established a TDD cycle within the complex AEM technology stack using a "unified testing API". It illustrates how such an API can be built and discusses various advantages over other approaches such as the Sling Testing API.
9. Testing Text Image Component
AEM
Resources
App
TextImage
TextImage
Test
<creates>
<tests>
10. AEM Sling Testing API
@Before
public void setUp() throws Exception {
URL url = new URL(serverUrl);
httpClient = new HttpClient();
httpClient.getParams().setAuthenticationPreemptive(true);
httpClient.getState().setCredentials(new AuthScope(url.getHost(), url.getPort(), ANY_REALM), getCredentials());
client = new SlingIntegrationTestClient(httpClient);
}
private void createPage(String path, Properties properties, Template template) throws Exception {
String[] pair = splitPathName(path);
HttpMethod createReq = new PostMethod(serverUrl + "/bin/wcmcommand");
createReq.setQueryString(new Properties().append("cmd", “createPage").append("title", pair[1])
.append("parentPath", pair[0]).append(TEMPLATE, template.getName()).toPairs());
if (HttpStatus.SC_OK != httpClient.executeMethod(createReq)) {
throw new HttpResponseException(createReq.getStatusCode(), createReq.getStatusText());
}
if (!properties.isEmpty()) {
HttpMethod changeReq = new PostMethod(serverUrl + path + "/" + JCR_CONTENT);
changeReq.setQueryString(properties.toPairs(true));
if (HttpStatus.SC_OK != httpClient.executeMethod(changeReq)) {
throw new HttpResponseException(changeReq.getStatusCode(), changeReq.getStatusText());
}
}
}
private String createResource(String path, Properties properties) throws IOException {
String nodeUrl = serverUrl + path;
client.createNode(nodeUrl, properties.<String>toMap());
return nodeUrl;
}
11. AEM Sling Testing API
@Before
public void setUp() throws Exception {
URL url = new URL(serverUrl);
httpClient = new HttpClient();
httpClient.getParams().setAuthenticationPreemptive(true);
httpClient.getState().setCredentials(new AuthScope(url.getHost(), url.getPort(), ANY_REALM), getCredentials());
client = new SlingIntegrationTestClient(httpClient);
}
private void createPage(String path, Properties properties, Template template) throws Exception {
String[] pair = splitPathName(path);
HttpMethod createReq = new PostMethod(serverUrl + "/bin/wcmcommand");
createReq.setQueryString(new Properties().append("cmd", “createPage").append("title", pair[1])
.append("parentPath", pair[0]).append(TEMPLATE, template.getName()).toPairs());
if (HttpStatus.SC_OK != httpClient.executeMethod(createReq)) {
throw new HttpResponseException(createReq.getStatusCode(), createReq.getStatusText());
}
if (!properties.isEmpty()) {
HttpMethod changeReq = new PostMethod(serverUrl + path + "/" + JCR_CONTENT);
changeReq.setQueryString(properties.toPairs(true));
if (HttpStatus.SC_OK != httpClient.executeMethod(changeReq)) {
throw new HttpResponseException(changeReq.getStatusCode(), changeReq.getStatusText());
}
}
}
private String createResource(String path, Properties properties) throws IOException {
String nodeUrl = serverUrl + path;
client.createNode(nodeUrl, properties.<String>toMap());
return nodeUrl;
}
Setup Client
12. AEM Sling Testing API
@Before
public void setUp() throws Exception {
URL url = new URL(serverUrl);
httpClient = new HttpClient();
httpClient.getParams().setAuthenticationPreemptive(true);
httpClient.getState().setCredentials(new AuthScope(url.getHost(), url.getPort(), ANY_REALM), getCredentials());
client = new SlingIntegrationTestClient(httpClient);
}
private void createPage(String path, Properties properties, Template template) throws Exception {
String[] pair = splitPathName(path);
HttpMethod createReq = new PostMethod(serverUrl + "/bin/wcmcommand");
createReq.setQueryString(new Properties().append("cmd", “createPage").append("title", pair[1])
.append("parentPath", pair[0]).append(TEMPLATE, template.getName()).toPairs());
if (HttpStatus.SC_OK != httpClient.executeMethod(createReq)) {
throw new HttpResponseException(createReq.getStatusCode(), createReq.getStatusText());
}
if (!properties.isEmpty()) {
HttpMethod changeReq = new PostMethod(serverUrl + path + "/" + JCR_CONTENT);
changeReq.setQueryString(properties.toPairs(true));
if (HttpStatus.SC_OK != httpClient.executeMethod(changeReq)) {
throw new HttpResponseException(changeReq.getStatusCode(), changeReq.getStatusText());
}
}
}
private String createResource(String path, Properties properties) throws IOException {
String nodeUrl = serverUrl + path;
client.createNode(nodeUrl, properties.<String>toMap());
return nodeUrl;
}
Create
Page
13. AEM Sling Testing API
@Before
public void setUp() throws Exception {
URL url = new URL(serverUrl);
httpClient = new HttpClient();
httpClient.getParams().setAuthenticationPreemptive(true);
httpClient.getState().setCredentials(new AuthScope(url.getHost(), url.getPort(), ANY_REALM), getCredentials());
client = new SlingIntegrationTestClient(httpClient);
}
private void createPage(String path, Properties properties, Template template) throws Exception {
String[] pair = splitPathName(path);
HttpMethod createReq = new PostMethod(serverUrl + "/bin/wcmcommand");
createReq.setQueryString(new Properties().append("cmd", “createPage").append("title", pair[1])
.append("parentPath", pair[0]).append(TEMPLATE, template.getName()).toPairs());
if (HttpStatus.SC_OK != httpClient.executeMethod(createReq)) {
throw new HttpResponseException(createReq.getStatusCode(), createReq.getStatusText());
}
if (!properties.isEmpty()) {
HttpMethod changeReq = new PostMethod(serverUrl + path + "/" + JCR_CONTENT);
changeReq.setQueryString(properties.toPairs(true));
if (HttpStatus.SC_OK != httpClient.executeMethod(changeReq)) {
throw new HttpResponseException(changeReq.getStatusCode(), changeReq.getStatusText());
}
}
}
private String createResource(String path, Properties properties) throws IOException {
String nodeUrl = serverUrl + path;
client.createNode(nodeUrl, properties.<String>toMap());
return nodeUrl;
}
Setup Resource
14. Test: Image caption is shown
@Test
public void testGetImageLegendWithSourceAndCaptionReturnsBoth() throws Exception {
ResourceResolver resolver = new ResourceResolverImpl(client);
createPage(“/content/foobar”, new Properties(), PageType.PRESENCE.getTemplate());
createPage(“/content/foobar/ko”, new Properties(), PageType.LANGUAGE.getTemplate());
createPage(“/content/foobar/ko/home", new Properties(), PageType.HOME.getTemplate());
createPage(“/content/foobar/ko/home/content", new Properties(), PageType.DETAIL.getTemplate());
Resource contentPage = resolver.getResource(“/content/foobar/ko/home/content");
createResource(contentPage.getPath() + "/jcr:content/textimage", new Properties());
Resource target = resolver.getResource(contentPage.getPath() + "/jcr:content/textimage");
Properties imageProps = new Properties()
.append("source", "<sourceValue>")
.append("caption", "titleValue")
.append("sling:resourceType", ComponentType.IMAGE)
.append(JCR_LASTMODIFIED, new Time().format(DateFormat.GMT))
.append(JCR_LAST_MODIFIED_BY, "admin");
createResource(contentPage.getPath() + "/jcr:content/textimage/image", imageProps);
TextImageController testObj = new TextImageController().setup(createContext(target, contentPage));
assertEquals("titleValue<br /><sourceValue>", testObj.getImageLegend());
}
15. Test: Image caption is shown
@Test
public void testGetImageLegendWithSourceAndCaptionReturnsBoth() throws Exception {
ResourceResolver resolver = new ResourceResolverImpl(client);
createPage(“/content/foobar”, new Properties(), PageType.PRESENCE.getTemplate());
createPage(“/content/foobar/ko”, new Properties(), PageType.LANGUAGE.getTemplate());
createPage(“/content/foobar/ko/home", new Properties(), PageType.HOME.getTemplate());
createPage(“/content/foobar/ko/home/content", new Properties(), PageType.DETAIL.getTemplate());
Resource contentPage = resolver.getResource(“/content/foobar/ko/home/content");
createResource(contentPage.getPath() + "/jcr:content/textimage", new Properties());
Resource target = resolver.getResource(contentPage.getPath() + "/jcr:content/textimage");
Properties imageProps = new Properties()
.append("source", "<sourceValue>")
.append("caption", "titleValue")
.append("sling:resourceType", ComponentType.IMAGE)
.append(JCR_LASTMODIFIED, new Time().format(DateFormat.GMT))
.append(JCR_LAST_MODIFIED_BY, "admin");
createResource(contentPage.getPath() + "/jcr:content/textimage/image", imageProps);
TextImageController testObj = new TextImageController().setup(createContext(target, contentPage));
assertEquals("titleValue<br /><sourceValue>", testObj.getImageLegend());
}
Assemble
16. Test: Image caption is shown
@Test
public void testGetImageLegendWithSourceAndCaptionReturnsBoth() throws Exception {
ResourceResolver resolver = new ResourceResolverImpl(client);
createPage(“/content/foobar”, new Properties(), PageType.PRESENCE.getTemplate());
createPage(“/content/foobar/ko”, new Properties(), PageType.LANGUAGE.getTemplate());
createPage(“/content/foobar/ko/home", new Properties(), PageType.HOME.getTemplate());
createPage(“/content/foobar/ko/home/content", new Properties(), PageType.DETAIL.getTemplate());
Resource contentPage = resolver.getResource(“/content/foobar/ko/home/content");
createResource(contentPage.getPath() + "/jcr:content/textimage", new Properties());
Resource target = resolver.getResource(contentPage.getPath() + "/jcr:content/textimage");
Properties imageProps = new Properties()
.append("source", "<sourceValue>")
.append("caption", "titleValue")
.append("sling:resourceType", ComponentType.IMAGE)
.append(JCR_LASTMODIFIED, new Time().format(DateFormat.GMT))
.append(JCR_LAST_MODIFIED_BY, "admin");
createResource(contentPage.getPath() + "/jcr:content/textimage/image", imageProps);
TextImageController testObj = new TextImageController().setup(createContext(target, contentPage));
assertEquals("titleValue<br /><sourceValue>", testObj.getImageLegend());
}
Assemble
Act
17. Test: Image caption is shown
@Test
public void testGetImageLegendWithSourceAndCaptionReturnsBoth() throws Exception {
ResourceResolver resolver = new ResourceResolverImpl(client);
createPage(“/content/foobar”, new Properties(), PageType.PRESENCE.getTemplate());
createPage(“/content/foobar/ko”, new Properties(), PageType.LANGUAGE.getTemplate());
createPage(“/content/foobar/ko/home", new Properties(), PageType.HOME.getTemplate());
createPage(“/content/foobar/ko/home/content", new Properties(), PageType.DETAIL.getTemplate());
Resource contentPage = resolver.getResource(“/content/foobar/ko/home/content");
createResource(contentPage.getPath() + "/jcr:content/textimage", new Properties());
Resource target = resolver.getResource(contentPage.getPath() + "/jcr:content/textimage");
Properties imageProps = new Properties()
.append("source", "<sourceValue>")
.append("caption", "titleValue")
.append("sling:resourceType", ComponentType.IMAGE)
.append(JCR_LASTMODIFIED, new Time().format(DateFormat.GMT))
.append(JCR_LAST_MODIFIED_BY, "admin");
createResource(contentPage.getPath() + "/jcr:content/textimage/image", imageProps);
TextImageController testObj = new TextImageController().setup(createContext(target, contentPage));
assertEquals("titleValue<br /><sourceValue>", testObj.getImageLegend());
}
Assemble
Act
Assert
18. Integrated Tests with AEM
Author
Publish
boot2
JCR
JSP
JS/CSS
foo() : Foo
bar() : Bar
zip: Zip
zap : Zap
TextImage
testFooWithNull()
testFooWithOk()
testBarWithEmpty()
testBarWithResult()
TextImageTest
Request
Response
Client
Server
19. Integrated Tests with AEM
Round trip: ~80 tested features ~2m30s
Author
Publish
boot2
JCR
JSP
JS/CSS
foo() : Foo
bar() : Bar
zip: Zip
zap : Zap
TextImage
testFooWithNull()
testFooWithOk()
testBarWithEmpty()
testBarWithResult()
TextImageTest
Request
Response
Client
Server
20. It works, but still hurts!
Client Server
AEM
App
Test
Test
Test
Test
Test
21. It works, but still hurts!
Client Server
AEM
App
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
22. It works, but still hurts!
Client Server
AEM
App
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
TestTest
Test
Test
Test
Test
Test
Test
Test
Test
Delay (or prevent) feedback
Hard to write
Difficult to maintain
Flaky results
Low coverage
27. Builder API for Everything
Pages
Components
Resources
Assets
AemClient
TestSetup
TextImage
ControllerTest
<uses>
<creates>
<creates>
<creates><creates>
<creates>
28. TextImage Test with Builders
public class TextImageControllerTest extends IntegrationTestBase {
@Test
public void testGetImageLegendWithOnlySourceSetReturnsSource() throws Exception {
Resource contentPage = $.aDetailPageWithParents(“/content/foobar/ko/home/content”);
Resource target = $.aResource(contentPage.getPath() + "/jcr:content/textimage");
$.anImage(contentPage.getPath() + "/jcr:content/textimage/image","source","expected");
TextImageController testObj = new TextImageController().setup(
$.aPageContextWithComponent(target, contentPage));
assertEquals("expected", testObj.getImageLegend());
}
29. TextImage Test with Builders
public class TextImageControllerTest extends IntegrationTestBase {
@Test
public void testGetImageLegendWithOnlySourceSetReturnsSource() throws Exception {
Resource contentPage = $.aDetailPageWithParents(“/content/foobar/ko/home/content”);
Resource target = $.aResource(contentPage.getPath() + "/jcr:content/textimage");
$.anImage(contentPage.getPath() + "/jcr:content/textimage/image","source","expected");
TextImageController testObj = new TextImageController().setup(
$.aPageContextWithComponent(target, contentPage));
assertEquals("expected", testObj.getImageLegend());
}
Assemble
30. TextImage Test with Builders
public class TextImageControllerTest extends IntegrationTestBase {
@Test
public void testGetImageLegendWithOnlySourceSetReturnsSource() throws Exception {
Resource contentPage = $.aDetailPageWithParents(“/content/foobar/ko/home/content”);
Resource target = $.aResource(contentPage.getPath() + "/jcr:content/textimage");
$.anImage(contentPage.getPath() + "/jcr:content/textimage/image","source","expected");
TextImageController testObj = new TextImageController().setup(
$.aPageContextWithComponent(target, contentPage));
assertEquals("expected", testObj.getImageLegend());
}
Assemble
Act
31. TextImage Test with Builders
public class TextImageControllerTest extends IntegrationTestBase {
@Test
public void testGetImageLegendWithOnlySourceSetReturnsSource() throws Exception {
Resource contentPage = $.aDetailPageWithParents(“/content/foobar/ko/home/content”);
Resource target = $.aResource(contentPage.getPath() + "/jcr:content/textimage");
$.anImage(contentPage.getPath() + "/jcr:content/textimage/image","source","expected");
TextImageController testObj = new TextImageController().setup(
$.aPageContextWithComponent(target, contentPage));
assertEquals("expected", testObj.getImageLegend());
}
Assemble
Assert
Act
32. “Fluent Builder” makes use simple
• Fluent Builder API
• Conciseness: Fluent syntax, smart defaults, var args
• Uniformity: Consistent semantics
• Pervasiveness: Availability everywhere
Map<String, String> httpMapping = new Properties(
DATA_INPUT_SUB_PATH, new TestFile("file-delivery-test/input").getPath(),
DATA_OUTPUT_SUB_PATH, new TestFile("file-delivery-test/output").getPath()).toMap();
$.service(
new FileDeliveryService(),
$.aPathMappingService().withMapping(new PathMapping().putAll(httpMapping)));
<R> R service(R service, Object... requiredServices) throws Exception;
<R> R service(IServiceBuilder<R> builder, Object... requiredServices) throws Exception;
33. “Fluent Builder” makes use simple
• Fluent Builder API
• Conciseness: Fluent syntax, smart defaults, var args
• Uniformity: Consistent semantics
• Pervasiveness: Availability everywhere
Map<String, String> httpMapping = new Properties(
DATA_INPUT_SUB_PATH, new TestFile("file-delivery-test/input").getPath(),
DATA_OUTPUT_SUB_PATH, new TestFile("file-delivery-test/output").getPath()).toMap();
$.service(
new FileDeliveryService(),
$.aPathMappingService().withMapping(new PathMapping().putAll(httpMapping)));
<R> R service(R service, Object... requiredServices) throws Exception;
<R> R service(IServiceBuilder<R> builder, Object... requiredServices) throws Exception;
34. “Fluent Builder” makes use simple
• Fluent Builder API
• Conciseness: Fluent syntax, smart defaults, var args
• Uniformity: Consistent semantics
• Pervasiveness: Availability everywhere
Map<String, String> httpMapping = new Properties(
DATA_INPUT_SUB_PATH, new TestFile("file-delivery-test/input").getPath(),
DATA_OUTPUT_SUB_PATH, new TestFile("file-delivery-test/output").getPath()).toMap();
$.service(
new FileDeliveryService(),
$.aPathMappingService().withMapping(new PathMapping().putAll(httpMapping)));
<R> R service(R service, Object... requiredServices) throws Exception;
<R> R service(IServiceBuilder<R> builder, Object... requiredServices) throws Exception;
35. “Fluent Builder” makes use simple
• Fluent Builder API
• Conciseness: Fluent syntax, smart defaults, var args
• Uniformity: Consistent semantics
• Pervasiveness: Availability everywhere
Map<String, String> httpMapping = new Properties(
DATA_INPUT_SUB_PATH, new TestFile("file-delivery-test/input").getPath(),
DATA_OUTPUT_SUB_PATH, new TestFile("file-delivery-test/output").getPath()).toMap();
$.service(
new FileDeliveryService(),
$.aPathMappingService().withMapping(new PathMapping().putAll(httpMapping)));
<R> R service(R service, Object... requiredServices) throws Exception;
<R> R service(IServiceBuilder<R> builder, Object... requiredServices) throws Exception;
36. “Dynamic Proxy” makes it available
Assets
<creates>
<creates>
Pages
Components
Resources
AemClient
Integration
TestBase
TextImage
ControllerTest
<creates><creates>
<creates>
37. “Dynamic Proxy” makes it available
ITestSetup
IPagesIComponents IResourcesIAssetsIAemClient
Assets
<creates>
<creates>
Pages
Components
Resources
AemClient
Integration
TestBase
TextImage
ControllerTest
<creates><creates>
<creates>
38. “Dynamic Proxy” makes it available
ITestSetup
IPagesIComponents IResourcesIAssetsIAemClient
Assets
<creates>
<creates>
Pages
Components
Resources
AemClient
Integration
TestBase
TextImage
ControllerTest
<creates><creates>
<creates>
<owns>
39. “Dynamic Proxy” makes it available
ITestSetup
IPagesIComponents IResourcesIAssetsIAemClient
<creates>
<creates> MockPages
MockComponents
MockResources
MockAssets
MockClient
UnitTest
TestBase
<owns>
TextImage
ControllerTest
<creates>
<creates><creates>
40. Initialize Integration Test Context
public abstract class IntegrationTestBase {
public static final String MANDANT_TESTS = “/content/foobar”;
public static final Locale TEST_LOCALE = new Locale("ko");
private final Set<String> resourcesToDelete = new HashSet<>();
public ITestSetup $;
protected SlingClient slingClient;
protected ResourceResolver resolver;
@Before
public void setUpTestContext() throws Exception {
slingClient = new SlingClient(SERVER_URL);
resolver = new ResourceResolverImpl(slingClient);
IResources resources = new Resources(slingClient, resolver);
IPages pages = new Pages(slingClient, resolver);
IComponents components = new Components(resources);
$ = SetupFactory.create(ITestSetup.class).getSetup(resources, pages, components,
new Assets(resources), new AemClient(resolver), new Services(components));
slingClient.deleteResource(MANDANT_TESTS);
}
41. Initialize Integration Test Context
public abstract class IntegrationTestBase {
public static final String MANDANT_TESTS = “/content/foobar”;
public static final Locale TEST_LOCALE = new Locale("ko");
private final Set<String> resourcesToDelete = new HashSet<>();
public ITestSetup $;
protected SlingClient slingClient;
protected ResourceResolver resolver;
@Before
public void setUpTestContext() throws Exception {
slingClient = new SlingClient(SERVER_URL);
resolver = new ResourceResolverImpl(slingClient);
IResources resources = new Resources(slingClient, resolver);
IPages pages = new Pages(slingClient, resolver);
IComponents components = new Components(resources);
$ = SetupFactory.create(ITestSetup.class).getSetup(resources, pages, components,
new Assets(resources), new AemClient(resolver), new Services(components));
slingClient.deleteResource(MANDANT_TESTS);
}
42. Initialize Unit Test Context
public abstract class UnitTestBase {
public static final String MANDANT_TESTS = “/content/foobar”;
public static final Locale TEST_LOCALE = new Locale("ko");
public ITestSetup $;
@Before
public void setUpTestContext() throws Exception {
Locale.setDefault(TEST_LOCALE);
IAemClient client = new MockAemClient();
IResources resources = new MockResources(client);
IPages pages = new MockPages(resources, client);
IAssets assets = new MockAssets(resources, client);
IRequest request = new MockRequest(resources, client);
IComponents components = new MockComponents(resources);
$ = SetupFactory.create(ITestSetup.class).getSetup(client, resources, assets,
request, pages, components, new MockJspTags(resources, pages),
new MockServices());
}
43. Initialize Unit Test Context
public abstract class UnitTestBase {
public static final String MANDANT_TESTS = “/content/foobar”;
public static final Locale TEST_LOCALE = new Locale("ko");
public ITestSetup $;
@Before
public void setUpTestContext() throws Exception {
Locale.setDefault(TEST_LOCALE);
IAemClient client = new MockAemClient();
IResources resources = new MockResources(client);
IPages pages = new MockPages(resources, client);
IAssets assets = new MockAssets(resources, client);
IRequest request = new MockRequest(resources, client);
IComponents components = new MockComponents(resources);
$ = SetupFactory.create(ITestSetup.class).getSetup(client, resources, assets,
request, pages, components, new MockJspTags(resources, pages),
new MockServices());
}
44. Development Cycle with AEM
New
Integrated
Test
Copy Test
to
UnitTests
Adapt
Mocks
Test-drive
New Feature
47. Unit Testing with AEM
Mock AEM
JCR
foo() : Foo
bar() : Bar
zip: Zip
zap : Zap
TextImage
testFooWithNull()
testFooWithOk()
testBarWithEmpty()
testBarWithResult()
TextImageTest
Client
48. Unit Testing with AEM
Round trip: ~5000 unit tests ~30s
Mock AEM
JCR
foo() : Foo
bar() : Bar
zip: Zip
zap : Zap
TextImage
testFooWithNull()
testFooWithOk()
testBarWithEmpty()
testBarWithResult()
TextImageTest
Client
49. Conclusion: No excuses
• Best practices for initializing, stubbing and
mocking objects are made consistently available
• Test setups cost virtually nothing
• You write more simple, reliable, maintainable tests
• Listen to their feedback more frequently
• New developers quickly adapt to best practices
50. Yes, it’s possible
UT
Performance Testing
Integration
Testing
Human Testing
End-To-End Testing
HT
Integration Testing
Performance
Testing
Unit Testing
End-To-End Testing