Writing Integration and Unit Tests with SAP Hybris

Hybris provides a basic infrastructure for your unit and integration tests. To write meaningful tests with deterministic results the following recommendations should be used.

Common Tips

  • Keep your test cases simple (KISS principle). If you have many short test methods, you will find out easily what is wrong in your system and you won't need to debug your test.
  • Decide at the beginning if you are writing an integration or an unit test.
  • Test with boundary values. For example, if your class searches objects, you are going to find more bugs by testing the cases with null, one, number of elements of the first page and first page plus one, than with any other test case.  
  • Don't use mocks with integration tests.
  • Don't use partial mocks (spies) in your tests. They add complexity to your tests and they aren't required if your class has a single responsibility.

Unit Tests

Definition

Its goal is to check that the implementation of one isolated class fulfills the requirements. This class usually has a complex algorithm. In unit tests all the classes surrounding the tested class are either mocks (or simple POJOs which return primitives types). 

Tips

  • Don't use the service layer for this type of tests. Your class must not inherited from HybrisJUnit4Test or any of its subclasses.
  • Use Mockito, which comes with Hybris, for your unit tests.
  • Instantiate the tested class using the new constructor. Add the annotation InjectMocks to this field.
  • Define a private field for each of the classes called by your tested class. Add the anotation Mock to this fields.
  • Use the mockito runner which creates the mocks from the annotations and finds many errors when using the mockito framework
  • Instantiate the models using Mockito.mock(). if you use the new constructor you are going to get a NullPointerException when you call the setter of a localized attribute.
  • Use when().then*() to mock the calls to the secondary classes.
  • Use the Hybris anotation UnitTest. This let you use filters when you run tests using ant.

Limitations

  • If you want to test the logging, you have to add a HybrisLogListerner to the HybrisLogger. In Hybris 6.0 the log4j logAppenders don't work

Example

Here is an example from Areco Deployment Script Manager:

package org.areco.ecommerce.deploymentscripts.core.impl;
 
import de.hybris.bootstrap.annotations.UnitTest;
import de.hybris.platform.servicelayer.tenant.MockTenant;
 
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
 
import junit.framework.Assert;
 
import org.areco.ecommerce.deploymentscripts.core.TenantDetector;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
 
/**
 * It checks that the script configuration reader is handling correctly the conversion of the tenants.
 * 
 * @author arobirosa
 * 
 */
@UnitTest //Mark this test as an unitary test.
@RunWith(MockitoJUnitRunner.class) //With this runner Mockito creates the Mocks and inject them in the tested instance.
public class TenantConversionInScriptConfigurationTest { //Don't inherite from HybrisJUnit4Test
 
    private static final String JUNIT_TENANT_SCRIPT_NAME = "20141004_JUNIT_TENANT";
 
    private static final String MASTER_TENANT_SCRIPT_NAME = "20141004_MASTER_TENANT";
 
    private static final String DEPLOYMENT_SCRIPTS_FOLDER = "test/tenant-conversion-script-configuration/";
 
    @InjectMocks //Required by Mockito.
    private final PropertyFileDeploymentScriptConfigurationReader configurationReader = new PropertyFileDeploymentScriptConfigurationReader() { //Instance to be tested.
 
        @Override
        protected PropertyFileDeploymentScriptConfiguration createConfiguration() {
            return new PropertyFileDeploymentScriptConfiguration();
        }
    };
 
    @Mock //Mark this a mock which is going to be injected into the tested instance.
    private TenantDetector tenantDetector; //Object called by the tested instance.
 
    @Before
    public void setUp() {
        // We don't need MockitoAnnotations.initMocks(this); because use are using the runner
 
        // In this method you can define all Mockito.when() conditions which are common to all test cases 
    }
 
    @Test //Mark this method as testeable.
    public void testJunitTenantInSingleTenantEnvironment() throws URISyntaxException {        //Prepare the mock to reproduce the test case.
        // Given - Preparation of test data
        Mockito.when(tenantDetector.areWeInATestSystemWithOneSingleTenant()).thenReturn(Boolean.TRUE);
        Mockito.when(tenantDetector.getCurrentTenant()).thenReturn(new MockTenant("master"));
        Mockito.when(tenantDetector.getTenantByID(Mockito.eq("junit"))).thenReturn(null);
        // When and Then - Execution and assertion
        assertTenantConversion("master", JUNIT_TENANT_SCRIPT_NAME);
    }
 
    @Test
    public void testMasterTenantInSingleTenantEnvironment() throws URISyntaxException {
        // Given
        Mockito.when(tenantDetector.areWeInATestSystemWithOneSingleTenant()).thenReturn(Boolean.TRUE);
        Mockito.when(tenantDetector.getCurrentTenant()).thenReturn(new MockTenant("master"));
        Mockito.when(tenantDetector.getTenantByID(Mockito.eq("master"))).thenReturn(new MockTenant("master"));
        // When and Then
        assertTenantConversion("unexistentMaster", MASTER_TENANT_SCRIPT_NAME);
    }
 
    @Test
    public void testJunitTenantInMultiTenantEnvironment() throws URISyntaxException {
        // Given
        Mockito.when(tenantDetector.areWeInATestSystemWithOneSingleTenant()).thenReturn(Boolean.FALSE);
        Mockito.when(tenantDetector.getCurrentTenant()).thenReturn(new MockTenant("junit"));
        Mockito.when(tenantDetector.getTenantByID(Mockito.eq("junit"))).thenReturn(new MockTenant("junit"));
        // When and Then
        assertTenantConversion("junit", JUNIT_TENANT_SCRIPT_NAME);
    }
 
    @Test
    public void testMasterTenantInMultiTenantEnvironment() throws URISyntaxException {
        // Given
        Mockito.when(tenantDetector.areWeInATestSystemWithOneSingleTenant()).thenReturn(Boolean.FALSE);
        Mockito.when(tenantDetector.getCurrentTenant()).thenReturn(new MockTenant("master"));
        Mockito.when(tenantDetector.getTenantByID(Mockito.eq("master"))).thenReturn(new MockTenant("master"));
        // When and Then
        assertTenantConversion("master", MASTER_TENANT_SCRIPT_NAME);
    }
 
    private void assertTenantConversion(final String expectedTenantID, final String deploymentScriptNameD) throws URISyntaxException {
        final URL scriptUrl = this.getClass().getClassLoader().getResource(DEPLOYMENT_SCRIPTS_FOLDER + deploymentScriptNameD);
        // When - Execution of the method to test
        final PropertyFileDeploymentScriptConfiguration actualConfiguration = configurationReader.loadConfiguration(new File(scriptUrl.toURI()));
        // Then - Assertions
        Assert.assertEquals("The must be one tenant", 1, actualConfiguration.getAllowedTenants().size()); //Check the actual results vs. the expected results.
        Assert.assertEquals("The tenant has the wrong ID", expectedTenantID, actualConfiguration.getAllowedTenants().iterator().next().getTenantID());
    }
}

Integration Tests

Definition

This type of tests check that a class is working correctly together with other classes. If the class is a DAO this is the only meaningful type of the test for it. In Hybris all integration tests use the database which must have test data.

Tips

  • The test must inherited from ServicelayerTransactionalTest. Hybris will start a transaction when the test begin and do a rollback at the end. How transactions in integraton tests work
  • Instantiate all the models using modelService.create(ModelClass.class); This ensures that Hybriis removes this model when the test ends.
  • Use the annotation Resource to inject beans by name. The injection mechanism was built by Hybris in the class ServicelayerBaseTest, it isn't pure Spring.
  • Use the methods ServicelayerTest.importStream() and ServicelayerTest.importCsv() to import the test data required by your test.
  • If many integration tests required the same data, use Init Deployment Scripts to import them during the initialization of the junit tenant.
  • If your test needs its own spring beans you can use to load your test Spring application context:
@IntegrationTest
@AppendSpringConfiguration("/test/test-trainingextension-spring.xml")
public class CustomDeliveryCostsServiceTest extends ServicelayerTransactionalTest
{
(...)

The custom application file is located in hybris/bin/custom/trainingextension/resources/test/test-trainingextension-spring.xml

Limitations

  • The web application context aren't started. There is no way to test a controller using integration tests. A UI Test, for example with Selenium, is the right choice in this case.
  • Because the logging of the cronjobs into the database don't work inside a transactional integration test, you must test the class which implements JobPerformable with an empty cronjob model. If your cronjob doesn't use service layer jobs,  you must migrate it.
  • If you want to test the logging, you have to add a HybrisLogListerner to the HybrisLogger. In Hybris 6.0 the log4j logAppenders don't work
  • Subclasses of ServicelayerTransactionalTest can't load application contexts using the annotation @ContextConfiguration, please use @AppendSpringConfiguration

Example

Here is an example from Areco Deployment Script Manager:

package org.areco.ecommerce.deploymentscripts.core;
 
import de.hybris.bootstrap.annotations.IntegrationTest;
import de.hybris.platform.servicelayer.ServicelayerTransactionalTest;
import org.apache.log4j.Logger;
import org.areco.ecommerce.deploymentscripts.testhelper.DeploymentConfigurationSetter;
import org.areco.ecommerce.deploymentscripts.testhelper.DeploymentScriptResultAsserter;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
 
import javax.annotation.Resource;
 
/**
 * It restores the configuration of the manager, for example, the folders after the test.
 *
 * @author arobirosa
 */
@IntegrationTest //Mark the subclasses as integration tests
@Ignore //This is a base class for tests.
public abstract class AbstractWithConfigurationRestorationTest extends ServicelayerTransactionalTest {  //All the subclasses of this test use transactions.
        /*
         * Logger of this class.
         */
        private static final Logger LOG = Logger.getLogger(AbstractWithConfigurationRestorationTest.class);
 
        @Resource //Injection of beans
        private DeploymentScriptResultAsserter deploymentScriptResultAsserter;
        @Resource
        private DeploymentScriptStarter deploymentScriptStarter;
 
        @Resource
        private ScriptExecutionResultDAO flexibleSearchScriptExecutionResultDao;
 
        @Resource
        private DeploymentConfigurationSetter deploymentConfigurationSetter;
 
        @Before //We prepare the data or configuration for the test
        public void saveOldFolders() {
                if (LOG.isInfoEnabled()) {
                        LOG.info("Saving current folders");
                }
                deploymentConfigurationSetter.saveCurrentFolders();
        }
 
        @After //We restore the old configuration
        public void restoreOldFolders() {
                if (LOG.isInfoEnabled()) {
                        LOG.info("Restoring old folders");
                }
                // We don't want to affect other tests
                this.deploymentConfigurationSetter.restoreOldFolders();
        }
 
        @After
        public void resetErrorFlag() {
                if (LOG.isInfoEnabled()) {
                        LOG.info("Clearing error flag.");
                }
                this.deploymentScriptStarter.clearErrorFlag();
        }
 
        protected DeploymentConfigurationSetter getDeploymentConfigurationSetter() {
                return deploymentConfigurationSetter;
        }
 
        protected DeploymentScriptResultAsserter getDeploymentScriptResultAsserter() {
                return deploymentScriptResultAsserter;
        }
 
        protected DeploymentScriptStarter getDeploymentScriptStarter() {
                return deploymentScriptStarter;
        }
 
        protected ScriptExecutionResultDAO getFlexibleSearchScriptExecutionResultDao() {
                return flexibleSearchScriptExecutionResultDao;
        }
}
 
package org.areco.ecommerce.deploymentscripts.impex.impl;
 
import de.hybris.platform.core.model.order.price.TaxModel;
import de.hybris.platform.order.daos.TaxDao;
 
import java.util.List;
import java.util.Locale;
 
import javax.annotation.Resource;
 
import junit.framework.Assert;
 
import org.areco.ecommerce.deploymentscripts.core.AbstractWithConfigurationRestorationTest;
import org.areco.ecommerce.deploymentscripts.core.DeploymentScriptStarter;
import org.areco.ecommerce.deploymentscripts.testhelper.DeploymentScriptResultAsserter;
import org.junit.Test;
 
/**
 * It checks if impex scripts with different locales are correctly imported.
 * 
 * @author arobirosa
 * 
 */
// PMD doesn't see the assert in the private methods.
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
public class ImpexScriptWithLocaleTest extends AbstractWithConfigurationRestorationTest {
    private static final String RESOURCES_FOLDER = "/resources/test/impex-scripts-with-locale";
 
    @Resource
    private DeploymentScriptStarter deploymentScriptStarter;
 
    @Resource
    private DeploymentScriptResultAsserter deploymentScriptResultAsserter;
 
    @Resource
    private TaxDao taxDao;
 
    @Test  //Mark this method as testeable.
    public void testGermanLocale() {
        this.assertValueOfImportedTax("germany", "dummyGermanTax", 4.90d, Locale.GERMAN);
    }
 
    private void assertValueOfImportedTax(final String directoryCode, final String taxCode, final double expectedTaxValue, final Locale currentLocale) {
        this.getDeploymentConfigurationSetter().setTestFolders(RESOURCES_FOLDER, directoryCode, null);
        this.getDeploymentConfigurationSetter().setImpexLocaleCode(currentLocale.toString());
        final boolean wereThereErrors = this.deploymentScriptStarter.runAllPendingScripts(); //Run the tested functionality
        Assert.assertFalse("There were errors", wereThereErrors); //Check the actual results vs. the expected ones.
        deploymentScriptResultAsserter.assertSuccessfulResult("20141003_DUMMY_TAX");
        final List<TaxModel> foundTaxes = this.taxDao.findTaxesByCode(taxCode);
        Assert.assertEquals("There must be one tax", 1, foundTaxes.size());
        Assert.assertEquals("The imported value of the tax is wrong", expectedTaxValue, foundTaxes.iterator().next().getValue().doubleValue(), 0.001d);
    }
 
    @Test
    public void testAmericanLocale() {
        this.assertValueOfImportedTax("usa", "dummyAmericanTax", 18.342d, Locale.ENGLISH);
    }
}

– Based on SAP Commerce 2211

Discussion

Enter your comment. Wiki syntax is allowed: