Overview
The Nuxeo Test Framework is based on JUnit4 and provides features such as:
- Test configuration through annotations,
- Embedded Nuxeo Runtime,
- WebDriver support (and optional Concordion support),
- Guice injection of Nuxeo services and other configuration objects (injection is available only in test classes),
- High extensibility and test configuration re-usability.
At the time of writing, the test framework depends on JUnit 4.12, Guice 3.0, WebDriver 2.53.0 and HTML Unit Driver 2.20.
To use the test framework you must launch your test using the Nuxeo JUnit4 runner: FeaturesRunner
.
Example:
import org.nuxeo.runtime.test.runner.FeaturesRunner;
@RunWith(FeaturesRunner.class)
public class MyTest {
...
@Test
public void doSomeTest() { ... }
...
}
The org.nuxeo.runtime:nuxeo-runtime-test
module is required.
<dependency>
<groupId>org.nuxeo.runtime</groupId>
<artifactId>nuxeo-runtime-test</artifactId>
<scope>test</scope>
</dependency>
Depending on which features you want to use, you can also add the following dependencies:
- Core features:
org.nuxeo.ecm.core:nuxeo-core-test
- Platform features
org.nuxeo.ecm.platform:nuxeo-platform-test
- All Nuxeo features
org.nuxeo.ecm.platform:nuxeo-features-test
Test Features
The Nuxeo test framework uses special classes called test features to configure how tests are run. The test runner notifies the test feature of the different lifecycle events in the execution. The test setup or method is then customized accordingly. Test features are configurable through annotations and each test feature can use its own defined annotations to configure the test. A feature may provide additional objects to be injected in tests using Guice.
Here are some commonly used features:
org.nuxeo.runtime.test.runner.web.WebDriverFeature
org.nuxeo.runtime.test.runner.RuntimeFeature
org.nuxeo.runtime.test.runner.JettyFeature
org.nuxeo.ecm.core.test.CoreFeature
org.nuxeo.ecm.platform.test.PlatformFeature
org.nuxeo.ecm.webengine.test.WebEngineFeature
org.nuxeo.runtime.test.runner.distrib.DistributionFeature
See the org.nuxeo.runtime.test.runner.RunnerFeature
implementations for a full list.
To configure your test to use a specific test feature, add the @Features
annotation on your test class and specify the feature (or the list of features) needed.
Example:
@RunWith(FeaturesRunner.class)
@Features(RuntimeFeature.class)
public class MyTest {
...
}
Example on using multiple features:
@RunWith(FeaturesRunner.class)
@Features({RuntimeFeature.class, WebDriverFeature.class})
public class MyTest {
...
}
The list of configuration annotations provided by features will be specified for each feature when available.
Injection
Injection is available only in test classes. You cannot use injection in Nuxeo code since the Nuxeo Platform doesn't support injection. The FeaturesRunner instance is provided by default through Guice injection, so that in your test case you can access the runner like so:
@RunWith(FeaturesRunner.class)
public class MyTest {
@Inject
FeaturesRunner runner;
...
@Test
public void doSomeTest() { ... }
...
}
Features may provide more injectable objects. The complete list of these objects will be specified for each feature when available.
Features
Test features may require other test features. This means that using a feature will automatically include all the required features. So it's redundant to specify features that are already included by other features specified in your test. For example, the features declaration in the following code is useless: the CoreFeature
requires the RuntimeFeature
(so it's already included).
@RunWith(FeaturesRunner.class)
@Features({RuntimeFeature.class, CoreFeature.class})
public class MyTest {
...
}
The correct usage of the CoreFeature
:
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
public class MyTest {
...
}
Each feature provides its own configuration annotations and a list of services or objects available through Guice injection.
WebDriver Feature
Starts WebDriver before the tests are launched and stops it at the end of the tests.
Requirements
Top-level feature; does not require other features.
Configuration
@Browser
- configures the WebDriver instance.- BrowserFamily type - the driver type. Can be one of: FIREFOX, IE, CHROME, HTML_UNIT, HTML_UNIT_JS.
- Class factory - optional driver factory class to create custom drivers.
@HomePage
- to specify the home page (the entry point in the site to test)- Class page - the page to use as the home page
- String url - the URL to be bound to the home page (the site home URL)
Injectable Objects
- The WebDriver instance.
- The Configuration instance used to configure WebDriver. Can be used to introspect WebDriverFeature configuration.
- The WebPage instance of the home page. Must be used to get the entry point to the site.
Usage
WebDriver should be used in conjunction with the Page Provider pattern. Below is an example that tests Google search. First, we need to create a page object corresponding to the Google home:
public class GoogleHomePage extends WebPage {
@FindBy(how = How.NAME, using = "q")
private WebElement search;
public SearchResultPage search(String text) {
search.clear();
search.sendKeys(text);
search.submit();
return getPage(SearchResultPage.class);
}
}
When using the page factory pattern you should extend the abstract WebPage provided by the Nuxeo test framework which already has the driver instance injected inside. The WebPage abstraction also provides helper methods to quickly access WebDriver find methods (you also have a "wait until element is found" mechanism to block the test until the page is loaded and the given element is found).
From a WebPage object you can instantiate other page objects by calling getPage(MyPage.class)
. Pages instantiated this way will be injected with the Guice injector of the runner. Note that pages retrieved this way are cached so you don't need to cache them in a local variable in the test method.
When using the page factory pattern you must expose your site's functionality through page objects. The page objects are not the equivalent of a real web page. They should be used to map services offered by a WebPage and not the entire web page. For example you can design your home page to allow access to other standard 'sections' (i.e. pages) of your application like the log-in/log-out functionality, the menu etc. So typically a home page will provide methods to access sub pages of your WEB application and/or global functionality existing on the home page.
In our example, the home page provides a way of doing a search.
When testing web applications you must put all the details of web page mapping (like accessing elements in the DOM, clicking, filling in forms etc.) in your web page implementation. You shouldn't access these details directly from the test itself. This technique ensures that when you refactor a functionality of your web site you should only update the corresponding web page in your tests and not all the tests classes.
Now that we've defined our home page, let's define the search result page that is returned by the search method. This page gives access to the search result.
public class SearchResultPage extends WebPage {
@FindBy(how = How.CLASS_NAME, using = "l")
private WebElement firstLink;
public String getFirstResult() {
return firstLink.getText();
}
}
The search result page provides a method that returns the first link text in the search result.
Now we can write our Google search test like this:
@RunWith(FeaturesRunner.class)
@Features(WebDriverFeature.class)
@HomePage(type=MyHomePage.class, url="http://www.google.com")
@Browser(type=BrowserFamily.HTML_UNIT)
public class WebTest {
@Inject
protected MyHomePage home;
@Test
public void testSearch() throws Exception {
SearchResultPage result = home.search("test");
Assert.assertNotNull(result.getFirstResult());
}
}
By using the WebDriver Feature in conjunction with the WebEngine Feature you can test Nuxeo WebEngine modules that are deployed by the test framework without the need to point to an external URL. We will see this later in WebEngine Feature usage.
Runtime Feature
Starts a Nuxeo Test Server before the tests are launched and stops it at the end of the tests. All the tests launched by the test unit will run inside the same instance of Nuxeo Runtime.
Requirements
Top-level feature, does not require other features. After starting the server, all runtime bundles are deployed by the feature.
Configuration
@Deploy
- deploys a bundle (or an XML component from the given bundle) in the running server.- String value - the path of the bundle (or contribution). The path format is "
symbolicName:path
" wheresymbolicName
is the bundle symbolic name and the optional path is a path relative to the bundle root of the XML component to deploy. If no path is specified, the entire bundle will be deployed. Example: "org.nuxeo.runtime
", "org.nuxeo.runtime:OSGI-INF/my-component.xml
" etc.
- String value - the path of the bundle (or contribution). The path format is "
Note that @Deploy
is repeatable and should be used this way for more readability.
Injectable Objects
All Nuxeo services deployed using @Deploy
annotations will be available through Guice injector.
Usage
Example of how to test for the presence of a Nuxeo service:
@RunWith(FeaturesRunner.class)
@Features(RuntimeFeature.class)
public class ServiceTest {
@Inject
EventService eventService;
@Test
public void testService() throws Exception {
Assert.assertNotNull(eventService);
}
}
Example of how to use the @Deploy
annotations to deploy additional bundles and components:
@RunWith(FeaturesRunner.class)
@Features(RuntimeFeature.class)
@Deploy("org.nuxeo.ecm.core.schema")
@Deploy("org.nuxeo.runtime:OSGI-INF/my-test-contribution.xml")
public class DeployTest {
@Inject
SchemaManager schemas;
@Test
public void testService() throws Exception {
Assert.assertNotNull(schemas);
}
}
The first @Deploy
annotations will install the core schema bundle into the running server and the second @Deploy
will install the test resource located at OSGI-INF/my-test-contribution.xml
(the current class loader is used to locate the resource) into the existing nuxeo-runtime
bundle.
You should never deploy bundles using the RuntimeHarness API. Always use @Deploy
annotations to do so. The RuntimeHarness API can be useful while writing custom features but it should be used context wisely.
Jetty Feature
Starts a Jetty server embedded in the Nuxeo Test Server before the tests are launched and stops it at the end of the tests. This feature extends the Runtime feature and adds the nuxeo-runtime-jetty-adapter
to the deployments.
Requirements
Requires Runtime Feature
Configuration
@Jetty
- configures the Jetty server- String
host
default "localhost" - the host to bind to. - int
port
default 8080 - the port to bind to. - String
config
- optional attribute to specify a custom jetty.xml configuration file. - boolean
propagateNaming
default false - allow runtime fixture get access to the web-app naming context
- String
If no custom configuration is specified the default will be used (which also contains data-source JNDI bindings for various Nuxeo services).
Injectable Objects
None.
Usage
Starts Nuxeo server + embedded Jetty on port 9090:
@RunWith(FeaturesRunner.class)
@Features(JettyFeature.class)
@Jetty(port=9090)
public class JettyTest {
@Inject
org.mortbay.jetty.Server server;
@Test
public void jettyComponentIsDeployed() throws Exception {
assertNotNull(server);
}
}
If you want your tests to be run in a transaction context, you should use the Jetty Transactional Feature and instruct the JVM to use Nuxeo's naming factory by adding the following src/test/resources/jndi.properties
file in your project.
java.naming.factory.initial=org.nuxeo.runtime.jtajca.NamingContextFactory
java.naming.factory.url.pkgs=org.nuxeo.runtime.jtajca
Transactional Feature
Runs tests in a transaction context.
Requirements
The Transactional Feature is included in the Core Feature so you should only need to use it explicitly if you don't use Core Feature.
Configuration
@TransactionalConfig
- boolean
autostart
true - injects a new transaction before executing each test
- boolean
Runtime Stream Feature
Deploys the required Nuxeo stream bundles and configures a stream implementation to be used by tests.
Requirements
It requires Runtime Feature.
Configuration
This feature doesn't have any by test configuration.
nuxeo.test.stream
- system property to change stream implementation used by tests
WorkManager Feature
Deploys the required Nuxeo bundles and configures a work manager implementation to be used by tests.
Requirements
It requires Runtime Stream Feature.
Configuration
This feature doesn't have any by test configuration.
nuxeo.test.wokrmanager
- system property to change work manager implementation used by tests
Core Bulk Feature
Deploys the required Nuxeo bundles for bulk feature (BAF).
It leverages Runtime Stream feature to use configured stream implementation during tests.
Requirements
It requires Runtime Feature, Transactional Feature, Runtime Stream Feature and Core IO Feature.
Core Feature
Deploys the required Nuxeo core bundles and resources and configures a repository to be used by tests.
Requirements
The feature requires Runtime Feature, Transactional Feature and WorkManager Feature.
Configuration
@RepositoryConfig
- configures a Nuxeo Repository- Class
init
- aRepositoryInit
class to be used to initialize the repository content. - Granularity cleanup - the scope of the initialization done by the
init
class. Can be one of CLASS or METHOD. If METHOD is used the initialization will be done before each test method call. The default is CLASS but this may change in the future so you should always be explicit.
- Class
Injectable Objects
StorageConfiguration
- the repository storage configuration. This describes the repository capabilities.
Usage
@RunWith(FeaturesRunner.class)
@Features(CoreFeature.class)
@RepositoryConfig(init = DefaultRepositoryInit.class, cleanup = Granularity.CLASS)
public class SimpleSession {
@Inject
CoreSession session;
@Test
public void theSessionIsUsable() throws Exception {
assertNotNull(session);
assertNotNull(session.getDocument(new PathRef("/default-domain")));
}
}
This starts a Nuxeo repository, creates a CoreSession (using the "Administrator" user) and initializes the repository using the DefaultRepositoryInit initializer only once (CLASS level) before running the test.
Platform Feature
This feature extends the Core Feature and adds deployment of basic Nuxeo services like directories, user manager etc.
Requirements
Requires Core Feature.
Configuration
None
Injectable Objects
None
WebEngine Feature
Extends the Platform Feature and adds deployment of WebEngine core bundles (including Administration module). Also adds the WebDriver and Jetty features. You should use this feature when testing WebEngine UI.
Requirements
Requires Platform Feature, WebDriver Feature and Jetty Feature
May not work directly from Eclipse since it requires on its classpath the final WebEngine JARs -- as they are built with Maven -- to have generated type definitions in the META-INF directory. This will be fixed later in WebEngine.
Configuration
None
Injectable Objects
None
Usage
@RunWith(FeaturesRunner.class)
@Features(WebEngineFeature.class)
@Browser(type=BrowserFamily.FIREFOX)
@HomePage(type=WebEngineHomePage.class, url="http://localhost:8082")
@Jetty(port=8082)
public class WebEngineTest {
@Inject
private WebEngineHomePage home;
@Test
public void iCanRunWebEngine() throws Exception {
LoginPage login = home.getLoginPage();
assertTrue(home.hasModule("Admin"));
assertFalse(login.isAuthenticated());
login.ensureLogin("Administrator", "Administrator");
assertTrue(login.isAuthenticated());
AdminModulePage admin = home.getModulePage("Admin", AdminModulePage.class);
DocumentPage doc = admin.getDocumentPage("default-domain");
Assert.assertEquals("Default domain", doc.getTitle());
}
}
This test will start a Nuxeo with Jetty enabled at port 8082 - so WebEngine root will be located at http://localhost:8082. The @HomePage
used by WebDriver feature will point to the embedded WebEngine root.
This test respects the page factory pattern we discussed above: the web mapping details are inside the web page implementations. The test is not aware of the page structure -- it only uses well-named methods to access different functionalities.
If you want to have a look at these classes you can find this test in nuxeo-features-test
.
Automation Feature
Extends Platform Feature and adds Nuxeo Automation bundles (org.nuxeo.ecm.automation.core
and org.nuxeo.ecm.automation.feature
).
Dummy Client Login Feature
Login with any username without any check against usermanager
. Useful for low levels unit tests such as in directory, where it's not possible to add a dependency to usermanager
or for tests that don't setup directories. Each authenticated user will be available in the stack ClientLoginModule.getCurrentPrincipal();
By default only the group Everyone
is added to each user and it is not possible to add other groups.
To use it, just add the following dependency:
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-login</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
Requirements
Add the following dependency to use it:
<dependency>
<groupId>org.nuxeo.ecm.core</groupId>
<artifactId>nuxeo-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-directory-types-contrib</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.nuxeo.ecm.platform</groupId>
<artifactId>nuxeo-platform-login</artifactId>
<scope>test</scope>
</dependency>
Configuration
None
Injectable Objects
None
Usage
@RunWith(FeaturesRunner.class)
@Features(ClientLoginFeature.class)
public class TestDummyLogInClientModule {
@Inject
ClientLoginFeature login;
@Test
public void canGetCurrentDummyPrincipal() throws LoginException {
Principal dummyPrincipal = login.loginAs("dummyName");
NuxeoPrincipal currentDummy = ClientLoginModule.getCurrentPrincipal();
Assert.assertEquals(dummyPrincipal.getName(), currentDummy.getName());
}
}
LogCapture Feature
A feature to make assertions on Log4j output.
A Log4j appender is initialized and configured with the running Log4j instance. When any Log4j log method is called, the appender captures the logs if the logs match the configured filter. It's then possible to get the captured logs or assert if there is any captured log.
Requirements
None
Configuration
@LogCaptureFeature.FilterOn
- uses and configures the default filter. There are two parameters,logLevel
, andloggerName
. You can use one or both.@LogCaptureFeature.FilterWith
- configures the filter the logs have to match to be part of the results.
Injectable Objects
LogCaptureFeature.Result logCaptureResult
- the result containing the captured logs.
Usage
@RunWith(FeaturesRunner.class)
@Features({ CoreFeature.class, LogCaptureFeature.class })
@Deploy({ "org.nuxeo.ecm.automation.core" })
public class TestRestoreInputFromScriptAndLogOperation {
@Inject
AutomationService service;
@Inject
CoreSession session;
@Inject
LogCaptureFeature.Result logCaptureResult;
public static class MyLogFilter implements LogCaptureFeature.Filter {
@Override
public boolean accept(LoggingEvent event) {
// Will add to the result, only the logs that has the ERROR level and "loggerName" category
if (!event.getLevel().equals(Level.ERROR)) {
return false;
}
if (!event.getLoggerName().equals("loggerName")) {
return false;
}
return true;
}
}
@Test
@LogCaptureFeature.FilterWith(TestRestoreInputFromScriptAndLogOperation.MyLogFilter.class)
public void testRestoreInput() throws Exception {
DocumentModel doc = session.createDocumentModel("/", "test", "File");
doc.setPropertyValue("dc:title", "test");
doc = session.createDocument(doc);
OperationContext ctx = new OperationContext(session);
ctx.setInput(doc);
OperationChain chain = new OperationChain("testSetObjectInput");
chain.add(FetchContextDocument.ID);
chain.add(SetInputAsVar.ID).set("name", "test");
chain.add(FetchDocument.ID).set("value", "/");
chain.add(RestoreDocumentInputFromScript.ID).set("script", "Context[\"test\"]");
chain.add(LogOperation.ID).set("category", "loggerName").set("message",
"expr:Input title @{This.title}. next id : @{Fn.getNextId(\"pouet\")}").set(
"level", "error");
DocumentModel returnedDoc = (DocumentModel) service.run(ctx, chain);
Assert.assertEquals("/test", returnedDoc.getPathAsString());
// making sure that the log operation is using the ERROR level and is in the "loggerName" category
logCaptureResult.assertHasEvent();
}
@Test
@LogCaptureFeature.FilterOn(logLevel = "ERROR", loggerName = "loggerName")
public void testLogLevelAndLoggerName() throws NoLogCaptureFilterException{
DocumentModel doc = session.createDocumentModel("/", "test", "File");
doc.setPropertyValue("dc:title", "test");
doc = session.createDocument(doc);
OperationContext ctx = new OperationContext(session);
ctx.setInput(doc);
OperationChain chain = new OperationChain("testSetObjectInput");
chain.add(FetchContextDocument.ID);
chain.add(SetInputAsVar.ID).set("name", "test");
chain.add(FetchDocument.ID).set("value", "/");
chain.add(RestoreDocumentInputFromScript.ID).set("script", "Context[\"test\"]");
chain.add(LogOperation.ID).set("category", "loggerName").set("message",
"expr:Input title @{This.title}. next id : @{Fn.getNextId(\"pouet\")}").set(
"level", "error");
DocumentModel returnedDoc = (DocumentModel) service.run(ctx, chain);
Assert.assertEquals("/test", returnedDoc.getPathAsString());
// making sure that the log operation is using the ERROR level and is in the "loggerName" category
logCaptureResult.assertHasEvent();
}
Theme Feature
Extends WebEngine Feature and adds Nuxeo Theme deployment.
Requirements
Requires WebEngine Feature.
Configuration
None
Injectable Objects
None
Mockito Feature
You can access services in your test by:
- calling
Framework.getService(MyService.class)
- Use Guice
@Inject
annotation
This feature lets you override a service, and replace it with a mock.
Requirements
None.
Configuration
MockitoFeature
.- Use
@Mock
annotation on your test class attribute, so that the Mockito initializer does its job. - Use
@RuntimeService
annotation (org.nuxeo.runtime.mockito
) on the same test class attribute (injects a mock in the field and binds it to the the service manager).
Injectable Objects
All deployed Nuxeo services.
Usage
In the following example we replace the Directory Service
by a mock.
@RunWith(FeaturesRunner.class)
@Features({ MockitoFeature.class })
public class ProjectInitializerTest {
@Mock
@RuntimeService
DirectoryService dirServiceMock;
Creating Custom Features
When you are writing generic services that you export to other bundles you may want to provide a specific feature for you services to help others test code depending on your service. In most cases the feature will only extend an existing feature by adding new deployments. This is the case for example of the Platform Feature (not that a feature must implement the RunnerFeature interface -- in our case we extend SimpleFeature which is an empty feature):
@Deploy({
"org.nuxeo.ecm.platform.api",
"org.nuxeo.ecm.platform.content.template",
"org.nuxeo.ecm.platform.dublincore",
"org.nuxeo.ecm.directory",
"org.nuxeo.ecm.directory.sql",
"org.nuxeo.ecm.platform.usermanager.api",
"org.nuxeo.ecm.platform.usermanager",
"org.nuxeo.ecm.platform.test:test-usermanagerimpl/schemas-config.xml",
"org.nuxeo.ecm.platform.test:test-usermanagerimpl/directory-config.xml"
})
@Features(CoreFeature.class)
public class PlatformFeature extends SimpleFeature {
}
To list the required features you must use the @Features
annotation on your own feature. @Deploy
annotation is supported on features too (this is not the case for other annotations -- in fact it depends on how the feature that provides the annotation is scanning for them).
If you need more control you can refer to sources and/or existing feature implementations. Below you can find some typical use cases you may need when implementing a feature:
Custom Configuration Annotations and Guice Injection Providers
public class MyFeature extends SimpleFeature {
protected MyConfiguration config = null;
// create config. object from annotations
public void initialize(FeaturesRunner runner) throws Exception {
Class<?> clazz = runner.getTargetTestClass();
MyAnnotation anno = FeaturesRunner.getScanner().getFirstAnnotation(clazz, MyAnnotation.class);
if (anno == null) { // create an annotation using default values
anno = Defaults.of(MyAnnotation.class);
}
config = createConfigFromAnnotation(anno);
}
// configure Guice bindings
public void configure(FeaturesRunner runner, Binder binder) {
binder.bind(MyConfiguration.class).toInstance(config);
}
}
Annotations should be collected in the initialize method. The configure method is called after the start method and before the Guice injector is created.
Dynamic Bundle Deployment
@Features(RuntimeFeature.class)
public class MyFeature extends SimpleFeature {
protected MyConfiguration config = null;
// create config. object from annotations
public void initialize(FeaturesRunner runner) throws Exception {
config = createConfigFromAnnotation();
}
@Override
public void start(FeaturesRunner runner) throws Exception {
if (config.needToDeployBundleX) {
runner.getFeature(RuntimeFeature.class).getHarness().deployBundle("bundleX");
} else {
runner.getFeature(RuntimeFeature.class).getHarness().deployBundle("bundleY");
}
}
}
As you see you must add the requirement on the RuntimeFeature since you need to access the bundle deployment which is provided by this feature.
Even if it is possible, you must not access the underlying Runtime Harness directly from the test. This will make your test non-portable. You will not be able to use it with the Distribution Feature for example. The Runtime Harness is safe to be accessed only from features that depend on the Runtime feature.
Patch Nuxeo Server Home Directory
@Features(RuntimeFeature.class)
public class MyFeature extends SimpleFeature implements WorkingDirectoryConfigurator {
// add a work directory configurator on the harness
public void initialize(FeaturesRunner runner) throws Exception {
runner.getFeature(RuntimeFeature.class).getHarness().addWorkingDirectoryConfigurator(this);
}
public void configure(RuntimeHarness harness, File workingDir) throws IOException {
copyMyFilesTo(workingDirectory);
}
}
This feature depends on the Runtime Feature. It adds a working directory configurator in the initialization part which will be called when the framework is started and before bundles are deployed. No need to unregister the configurator.