-
Notifications
You must be signed in to change notification settings - Fork 39
Best practices
This document provides an unordered list of best practices for extending the framework and writing tests. Some of these advices might seem like common sense, some might not.
The testing client sprovide a lot of rules and clients for using one or multiple AEM instances in a generic way. It's also layered, from generic actions in sling to more specific actions in cq and granite (to be merge with cq).
If it looks like there might be a client action in the testing clients, use that.
- Use and compose the provided Junit rules. They're great! Like the CQAuthorPublishClassRule. Every test class should include the CQRule at method level and CQClassRule (or a derivative) at class level, to leverage some very useful, out-of-the-box functionalities, like test filtering
public class MyfeatureIT {
@ClassRule
public static CQAuthorClassRule cqBaseClassRule = new CQAuthorClassRule();
@Rule
public CQRule cqBaseRule = new CQRule(cqBaseClassRule.authorRule);
[...]
}
If ordering is needed, create a new chain rule that includes one of these (see example below).
- Avoid the
TestBase
parent class pattern, i.e. do not have test classes extending from a common base class. Anything you can do with parent classes and more, you can do with Junit Rules. Chaining rules looks like this:
public class BaseTestClassRule implements TestRule {
public BaseTestClassRule() {
super();
cqClassRule = new CQClassRule();
author1Rule = ClassRuleUtils.newInstanceRule(true).withRunMode("author");
author2Rule = ClassRuleUtils.newInstanceRule(true).withRunMode("author");
publishRule = ClassRuleUtils.newInstanceRule(true).withRunMode("publish");
ruleChain = RuleChain.outerRule(cqClassRule)
.around(author1Rule)
.around(author2Rule)
.around(publishRule);
}
[...]
@Override
public Statement apply(Statement base, Description description) {
return ruleChain.apply(base, description);
}
}
Then, instead of extending a BaseTest
class, apply a BaseTestClassRule
If you find yourself performing a certain action more than once, put that in a custom client. This is the best way to write reusable code for your application since extending from SlingClient
provides a lot of value (the whole framework is built around this concept). The best example of an extended client is CQClient which leverages all the benefits of SlingClient
and adds extra AEM-specific methods. If you feel a full blown client is overkill, you can create a simple utility class, but you should avoid as much as possible copy-pasting.
Try to use the most top-level clients and utilities provided by the framework - they provide a lot of stuff out-of-the-box (e.g. pointing to the topology under test, authentication, sharing the cookie store, timeouts, etc.)
Avoid using things like HttpGet
directly and use AbstractSlingClient#doGet.
As a rule of thumb, you should never use Thread.sleep()
. Always use AbstractPoller
or better yet, the newer Polling class for trying the action until it returns the expected result.
Always use a specific name-spaced paths. E.g. add tst-
prefix to all the resources created. You will later be able to easily identify and clean the generated content
Make sure tests don't leave side-effects behind. For example:
- Use OsgiInstanceConfig to set an osgi property and restore it at the end to the original value before the test
- Use the Page resource rule that deletes the page at the end
- Clean up/ restore in an
@After
or@AfterClass
anything that was created or changed in the test. Better yet, use a rule (it's like a reusable class that provides before and after )
When writing an end-to-end test, one often times needs to do things like prepare some content or create a user, in order to actually test a certain functionality.
In those cases, obviously separate the setup and cleanup (!) in before/ after methods or their class equivalents. Additionally, try and follow this rule:
- Setup and cleanup should be permissive - e.g. retry a page creation or deletion. Afterall, it's not what the test actually needs to test; there's a different test for page creation.
- On the other hand, make asserts strict - e.g. adding a component to that prepared page shoudl work the first time! (unless specified otherwise in documentation or the expectations of the feature)
- Use sensible values for Polling, based on the expectations.
- Max one order of magnitude higher timeouts - e.g. expected to create the user async in 100ms, you can poll every 250ms, for max 2.5 seconds.
- It's not enough if "it works on my machine". The tests are reused in multiple scenarios, on different types of deployments, with varying resources.
The http testing clients are meant for functional/ integrated http tests. They are relatively high on the testing pyramid, under UI tests, but clearly above unit tests. That means the complexity, the execution time and needed resources are quite high. Try covering as much as possible with unit tests and minimal integration tests. Try the OSGi mocks
or pax exam
, they're great!
InterruptedException
is thrown by blocking methods, e.g. Thread.sleep()
and it's a mechanism to gracefully terminate
a program. If this exception is inhibited (catch without rethrow), the program/test will not respect the SIGINT signal
(e.g. will not stop at CTRL+C). In corner cases, you might need to catch the exception to allow the method to finish
the task, but make sure you rethrow it at the end.
Besides this, throws InterruptedException
is a way to notify the user that the method is blocking (performing a wait).
FasterXML jackson is embedded in the library's core (up to sling testing clients, see SlingClient#doGetJson()
), so use it whenever you need to handle json data. This is to avoid the proliferation of json libraries that have the same scope and add up to the final size of the tests jar.
Converting a path an absolute url is very sensitive, since you have to take care of trailing slashes, context paths, encoding, http scheme and other subtleties. The rule of thumb is that you should never build the url or extract the path manually in the test, but instead use the helper methods SlingClient#getPath()
and SlingClient#getUrl()
.
Common cases where you would use this:
- Use
SlingClient#getUrl(String path)
- There's also a variant that accepts url parameters:
getUrl(String path, List<NameValuePair> parameters)
- Most methods of the testing clients work with paths, you should not use this only when using the url in external systems
String myPath = "/content/my/path";
SlingClient client = new SlingClient("http://localhost:4502/custom/", "user", "pass";
URI absoluteUrl = client.getUrl(myPath);
// absoluteUrl = "http://localhost:4502/custom/content/my/path"
- Use
SlingClient#getPath(String url)
- The returned path is not identical to
URI.getPath
, SlingClient it's aware of and extracts the context path
String myUrl = "http://localhost:4502/custom/content/my/path";
SlingClient client = new SlingClient("http://localhost:4502/custom/", "user", "pass";
URI path = client.getPath(myUrl);
// path = "/content/my/path"