Testing
Self-Managed Commerce includes an extensive suite of automated tests. This test coverage provides a quick and effective way to detect bugs that may have been introduced while making modifications. When customizing, it is good practice to write tests for new or modified code and run the relevant tests prior to committing changes.
Test Levels
Self-Managed Commerce supports four levels of tests. Each level tests functionality from a different perspective, and each level trades speed for realism.
| Level | Perspective | Framework | Maven Plugin | Naming |
|---|---|---|---|---|
| Unit tests | A single class, in isolation | JUnit, Mockito, AssertJ | Surefire | *Test.java |
| Integration tests | Multiple components working together with Spring and a database | JUnit, Spring Test, AssertJ | Failsafe | *Test.java, in *-itests modules |
| Cucumber tests | A business scenario, expressed in plain language | Cucumber | Failsafe | *.feature files |
| Selenium tests | A business user interacting with Commerce Manager in a browser | Cucumber, Selenium | Failsafe | *.feature files |
Unit Tests
Unit tests verify the behavior of a single class. The dependencies of the class under test are replaced with Mockito mocks, so the test controls every input the class receives and can verify every interaction the class makes. Unit tests do not start a Spring context, connect to a database, or depend on other services.
Unit tests are the fastest tests to run, and they are the first line of defense when modifying code. Most classes in Self-Managed Commerce have a corresponding unit test.
Integration Tests
Integration tests verify that multiple components work together correctly. They load a real Spring application context and persist data to an H2 database, so they detect problems that unit tests cannot, such as Spring configuration errors, incorrect OpenJPA mappings, invalid JPQL queries, and transaction boundary issues.
Integration tests are located in modules with names ending in -itests, such as ep-core-itests. Most integration tests extend BasicSpringContextTest or DbTestCase, and use test scenarios such as SimpleStoreScenario to populate the database with test data.
Cucumber Tests
Cucumber tests verify business scenarios. Each scenario is written in plain language in a .feature file, using the Given/When/Then syntax. Each step in the scenario is bound to a Java step definition that performs the action or verifies the outcome. Because the scenarios are readable by non-developers, they also serve as documentation of expected business behavior. Many Cucumber tests validate that Self-Managed Commerce APIs, such as the Cortex API and the Import/Export API, function as expected.
Self-Managed Commerce Cucumber tests run from two perspectives:
- Commerce Engine acceptance tests, such as those in
ep-core-acceptance-testsandep-importexport-acceptance-tests, call Commerce Engine services directly, running in the same process as the test. - System tests, such as those in
extensions/cortex/system-tests/cucumber, make requests to running Self-Managed Commerce services. For example, the Cortex system tests call the Cortex API over HTTP, verifying behavior from the perspective of a shopper using a storefront. For more information, see Extending System Tests.
Selenium Tests
Selenium tests are Cucumber tests whose step definitions use Selenium to drive Commerce Manager in a web browser. They verify behavior from the perspective of a business user, exercising the full stack from the user interface through to the database.
Selenium tests are located in the extensions/cm/ext-cm-modules/system-tests/selenium module. They require Commerce Manager and its dependent services to be running, and they are the slowest tests to run. For information about how Selenium tests locate Commerce Manager user interface elements, and how to make custom user interface elements testable, see Testing Commerce Manager Functionality.
Running Tests
For Maven commands that build Self-Managed Commerce with or without tests, see Elastic Path Maven Commands. For commands to run or debug a single unit, integration, Cucumber, or Selenium test, see Running Individual Tests.
Test reports are written to <artifact_Id>/target/surefire-reports for unit tests and <artifact_Id>/target/failsafe-reports for integration and Cucumber tests.
Adding New Tests
Adding New Unit Tests
The JUnit test case for a class is typically the name of the class with Test appended at the end. Test cases should be in the same package as the class they test so that they have protected access to the members in the class. To avoid cluttering the production code with unit test classes, place unit tests in a test directory structure that mirrors the main source folder. For example, the Cortex JUnit tests are in the same package structure as the java code they are testing, but they are in the "test" folder.
Test cases in Elastic Path typically use a method annotated with @BeforeEach to instantiate the object tested by each test in the test case. This method is also frequently used to configure any Mockito mock behavior that is not specific to any one test.
The Mockito framework facilitates testing of a single class in isolation. Mockito eases the creation of mock objects, which are test object instances that a class under test can reference. By using mocks, you can control the output that a class under test receives from objects it depends on so that you can test specific situations. You can also verify which methods the class under test invoked on a mock, including the parameters passed and the number of invocations.
Elastic Path recommends Mockito for all new unit tests.
note
Some existing Self-Managed Commerce unit tests use the JMock framework. Elastic Path is moving away from JMock, so do not use it in new tests. When making significant changes to a JMock test, consider converting it to Mockito.
Using Mockito
The following code shows a typical Mockito usage pattern with JUnit 5. In this example, we test the ProductImpl class and we use mock ProductType and AttributeValueGroup objects to test the interaction of ProductImpl with its dependencies.
@ExtendWith(MockitoExtension.class)
class ProductImplTest {
@Mock
private ProductType productType;
@Mock
private AttributeValueGroup attributeValueGroup;
@Mock
private AttributeGroup attributeGroup;
private final ProductImpl product = new ProductImpl();
@BeforeEach
void setUp() {
//Pass the mock objects to the product
product.setProductType(productType);
product.setAttributeValueGroup(attributeValueGroup);
}
@Test
void getAttributeValuesUsesProductTypeAttributeGroup() {
List<AttributeValue> expectedAttributeValues = Collections.singletonList(mock(AttributeValue.class));
//Specify what the mocks return when called by the product
when(productType.getProductAttributeGroup()).thenReturn(attributeGroup);
when(attributeValueGroup.getAttributeValues(attributeGroup, Locale.ENGLISH)).thenReturn(expectedAttributeValues);
//Invoke the method being tested, which will interact with the mock objects
List<AttributeValue> attributeValues = product.getAttributeValues(Locale.ENGLISH);
//Check the results of the operation
assertThat(attributeValues).isEqualTo(expectedAttributeValues);
verify(productType).getProductAttributeGroup();
}
}
When the class under test receives its dependencies through setters or a constructor, annotate the field holding the class under test with @InjectMocks. Mockito creates the instance and injects the @Mock fields into it.
Unused stubs cause test failures
By default, MockitoExtension uses strict stubs. If a test stubs a method with when(...) and the class under test never calls that method, the test fails with an UnnecessaryStubbingException. This keeps tests free of misleading setup code.
To resolve this error, remove the unused stub, or move it from the @BeforeEach method into only the tests that need it. If a stub is legitimately used by only some tests, wrap it with lenient():
lenient().when(productType.getProductAttributeGroup()).thenReturn(attributeGroup);
Adding New Integration Tests
Add integration tests to the -itests module that corresponds to the code under test. For example, add tests for ep-core to ep-core-itests, and tests for customizations in ext-core to ext-core-itests. Integration test classes are named with Test appended, like unit tests, and are run by Failsafe because they are located in an -itests module.
Most integration tests extend BasicSpringContextTest, which provides the following:
- A Spring application context, from which you can inject beans into the test with
@Autowired. - An H2 database, which you can populate with test data by calling
getTac().useScenario()with a test scenario class, such asSimpleStoreScenarioorCustomerScenario. - A database reset before each test class. To also reset the database after each test method that modifies data, including data created by a test scenario in a setup method, annotate the method or the test class with
@DirtiesDatabase.
Tests that work directly with the persistence engine, rather than with services, can extend DbTestCase. It extends BasicSpringContextTest, populates the database with SimpleStoreScenario, and provides helper methods for creating catalogs, stores, categories, and products.
BasicSpringContextTest uses a JUnit 4 runner, so integration tests that extend it must use JUnit 4 annotations, such as org.junit.Test and org.junit.Before.
public class CustomerServiceTest extends BasicSpringContextTest {
@Autowired
private CustomerService customerService;
private Customer customer;
@Before
public void setUp() {
//Populate the database with a customer
CustomerScenario scenario = getTac().useScenario(CustomerScenario.class);
customer = scenario.getCustomer();
}
@Test
@DirtiesDatabase
public void findByGuidReturnsPersistedCustomer() {
Customer foundCustomer = customerService.findByGuid(customer.getGuid());
assertThat(foundCustomer.getGuid()).isEqualTo(customer.getGuid());
}
}
Adding New Cucumber Tests
A Cucumber test consists of the following parts:
- A
.featurefile, containing one or more scenarios written in Given/When/Then syntax. - Step definition classes, containing methods annotated with
@Given,@When, or@Then. Each annotation contains a regular expression that matches a step in the feature file. Step definition classes are typically named with StepDefinitions appended. - A runner class named with IT appended, such as
RunCucumberIT, which Failsafe uses to run the feature files.
To add a Commerce Engine acceptance test, add the feature file to the src/test/cucumber/com/elasticpath/cucumber directory of the acceptance test module, such as ep-core-acceptance-tests, and add step definitions to the src/test/java/com/elasticpath/cucumber directory. Step definitions can inject Spring beans with @Autowired. Before writing a new step definition, check whether an existing step definition already matches the step, and reuse it.
To add a Cortex system test, see Extending System Tests.
Adding New Selenium Tests
Add custom Selenium tests to the extensions/cm/ext-cm-modules/ext-system-tests/selenium module, which contains example feature files, step definitions, and page objects. Selenium tests are organized as follows:
- Feature files are located in the
src/test/resources/com.elasticpath.cucumberdirectory. - Step definition classes are named with Definition appended, and are located in the
com.elasticpath.cucumber.definitionspackage. - Page objects, which represent Commerce Manager panes, editors, dialogs, and wizards, are located in the
com.elasticpath.seleniumpackage.
Step definitions and page objects provided by Elastic Path are located in the extensions/cm/ext-cm-modules/system-tests/common-test-definitions module, and can be reused by custom tests. The existing step definitions are also the best source of examples for using page objects and user interface widgets.
Page objects locate user interface elements by the automation IDs that Commerce Manager attaches to them. For more information, and for instructions on adding automation IDs to custom user interface elements, see Testing Commerce Manager Functionality.