
Abstract
#1 - Switch to OSGi Constructor Activation
OSGi R7 adds support for injecting referencces into a component's constructor. Why does this matter for tests?
In order to mock dependencies when using a @Reference annotation on a field, you must either:
Set a more permissive scope on the field - Exposing fields through overly permissive field restrictions breaks the encapsulation of your code
Use reflection to set the field - Reflection makes for more complex and fragile tests and makes code refactoring harder as you don't get compile-time exceptions when the code changes
Neither option is great, but instead with a constructor references, all of the services used by a component are injected when constructing the component. Let's see it in action! First, an example using field @Reference annotations:
public class BeforeComponent {
@Reference
private ResourceResolverFactory resolverFactory;
[... your code here...]
}
class BeforeComponentTest {
@Test
void testResourceResolverFactory() throws Exception {
BeforeComponentTest testObj = new BeforeComponentTest();
Field rrf = testObj.getClass().getDeclaredField("resolverFactory");
rrf.setAccessible(true);
rrf.set(testObj, mockResourceResolverFactory);
[...test code here...]
}
}
And the same class updated to use constructor references:
public class AfterComponent {
private final ResourceResolverFactory resolverFactory;
@Activate
public AfterComponent(@Reference ResourceResolverFactory resolverFactory) {
this.resolverFactory = resolverFactory;
}
[... your code here...]
}
class AfterComponentTest {
@Test
void testResourceResolverFactory() throws Exception {
AfterComponentTest testObj = new AfterComponentTest(mockResourceResolverFactory);
[...test code here...]
}
}
#2 - Upgrade to JUnit 5
Who wants to spend a bunch of time rewriting tests!?! Me neither, however JUnit 5 both great new features and an easy upgrade process via the junit-vintage-engine. This allows you to run your existing JUnit 3/4 tests alongside the new JUnit 5 Jupiter tests.
So what's the big deal about JUnit 5? In my opinion, there are two features that make JUnit 5 a huge productivity upgrade from JUnit 4.
Parametrized Tests
JUnit 4 has support for Parametrized Tests, but it's a pretty clunky process. With JUnit 5 you can easily implement common scenarios such as testing input validation with just annotations. For example if I wanted to test a node name validator, I could write a test like the following:
@ParameterizedTest
@ValueSource(strings = {"ns:ns2:id", "name/other"})
@NullAndEmptySource
void rejectsInvalidNames(String name) {
assertFalse(nameValidator.accept(name));
}
This just scratches the surface of what you can do with Parameterized Tests, but hopefully it gives you an idea of the capabilities of this feature.
Lambda Support
JUnit 5 adds Lambda support for Assertions, Assumptions and other testing features. This allows for some really neat features such as grouping assertions and asserting against exceptions within a test (rather than being the entire scope of the test).
@Test
void groupedAssertions() {
assertAll("can read",
() -> assertEquals("/content/dam", resource.getPath()),
() -> assertEquals("AEM Assets", resource.getValueMap().get("jcr:content/jcr:title","")
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(RepositoryException.class, () ->
doSomeJcrOperation(session));
assertTrue("SOME MESSAGE HERE", exception.getMessage());
}
#3 - Use Mockito for Simple Mocking
AEM is a complex beast. While you can certainly try to isolate your code as much as possible, there are still interactions with AEM that you'll need to test. Mockito enables you to mock services and application state without requiring instantiating the full dependency tree.
class SimpleMockedTest {
@Test
void testMockedObject() {
Resource myTestResource = mock(Resource.class);
when(myTestResource.getResourceType()).thenReturn("test/type");
System.out.println(myTestResource.getResourceType()); // will print test/type
}
}
Mockito's power really shines with answers and verifiers. While you may have mocks which can simply always return a value, with Mockito you can also verify that your mock's methods have been called, assert values passed to mocked methods or dynamically call code based on a mocked method execution.
class ComplexMockedTest {
@Test
void testMockedObject() {
ValueMap myValueMap = mock(ValueMap.class);
Map<String,String> values = Map.of("hello", "world");
when(myValueMap.get(anyString(), anyString())).thenAnswer(inv -> values.get(inv.get(0));
myMethodThatGetsFromTheValueMap();
verify(myValueMap.get(anyString(), anyString()));
}
}
#4 - Use Sling / AEM Mocks to Mock the Repository
Mocking has diminishing returns, especially for services where you don't own the contract or the contract isn't fixed. In these cases, bringing in Sling or AEM Mocks can vastly simplify the process of setting up a mocked environment and be a powerful tool for testing against a repository state.
Sling and AEM Mocks come loaded with the basic services you need as well as an empty mocked repository for loading content, you just need to add any custom (or non-standard) services and load the required content in your test setup:
@ExtendWith(SlingContextExtension.class)
public class ExampleTest {
private final SlingContext context = new SlingContext();
@BeforeEach
public void beforeEach(){
// initialize state
}
@Test
public void testSomething() {
Resource resource = context.resourceResolver().getResource("/content/sample/en");
// further testing
}
}
#5 - Reduce IT Flakiness with Awaitility
Integration tests in a CMS like AEM are... complex. Since AEM will renders markup based on the content provided to your code, writing ITs is challenging as you need to account for the markup variability.
AEM As a Cloud Service brings a bit more complexity due to it's cloud scalability. Due to the constraints of the CAP theorem, AEM as a Cloud Service trades Consistency for Availability and Partition Tolerance, e.g. that while changes may not appear immediately in AEM, it should always be available and should not fail due to networking issues.
The impact to integration tests is that unlike running a test against a local AEM instance, there's no guarantee that changes made in AEM as a Cloud Service are immediately reflected if your requests land on different servers running your instance.
Read Full Blog
Q&A
Please use this thread to ask the related questions.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.