In addition to manual test cases and unit tests, Elastic Path relies on FIT (Framework for Integrated Test). FIT allows testers to write tests without writing any code. FIT is a good framework for use with EP because we usually have a set of defined business rules to which we know the expected results and outcomes.
What is FIT?
Originally developed by Ward Cunningham, FIT is based on JUnit. Unlike JUnit, which focuses on the functionality of one method, FIT is designed for testing components. For example, a typical JUnit test might check the calculation of shipping costs, whereas a FIT test would check the functionality of the entire checkout process, including selecting a customer, adding a product to the shopping cart, calculating the shipping costs, and completing the checkout process.
What does a FIT test look like?
A FIT test consists of a set of HTML tables that perform various tasks. This generally includes:
- initializing the data required for the test
- performing some actions
- checking the results of those actions.
The following is an example of a FIT test:
| Back/Pre Order Additional Authorization. | |||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Description: Product is reserved. Qty onHand is 0. Inventory should be added successfully.
| |||||||||||||||||||||||||||||||||||||||||||
The test should always begin with a description of what the test is trying to accomplish. In this example, we are testing to see if adjusting inventory for a particular product works as expected.
The first table after the description points to a fixture. The fixture is a Java class that provides methods for use in the test. Every application that uses the FIT framework for testing has its own set of fixtures that are unique to its implementation. For example, the fixture that the test above references is PaymentServiceFixture. Therefore, we need a Java class named PaymentServiceFixture. Indeed, there is a class that exists with that name.
com.elasticpath.fit.PaymentServiceFixture |
So where is all the data? That's in the next table:
| use scenario | scenario/CA_Store.scn |
When the test is run, this table tells the FIT framework to call the useScenario(String) method in PaymentServiceFixture or if not found there, in a superclass. In this example, the method resides in EpDoFixture, which is a superclass of PaymentServiceFixture.
How does FIT know to call a method named useScenario() that's expecting one parameter? It concatenates all the words in the odd cells in the table, removes the spaces, can captializes the first letter of each word after the first one. Therefore, in the example above, it is looking for a useScenario() method with one parameter inside it. (If the first cell of the method contains the keyword "check", the behavior is a bit different; it concatenates the contents of the even cells and tries to look for a getter method with that name.)
The useScenario(String) is a custom method in Elastic Path Commerce that attempts to parse the specified .scn file. The file actually contains another FIT test. We use these scenario FIT tests to help populate the data required to perform other tests. If we look in CA_Store.scn, we can see that there are mostly tables that aid in the set up of a store.
The next step after adding the common required data is to add the test-specific data. We should be adding inventories to the store. However, looking through the code in PaymentServiceFixture.java would not reveal to us that this method is available to use, since it is not one of the methods within that class. However, there is a method in CatalogSetUpFixture.java that does just that. We have another custom method in Elastic Path that allows us to call an operation in another fixture. We use the operation by the keyword "use", followed by the name of the fixture in the next cell. After we declare this in the first row of a table, we can then use methods of that class. Note that you can call any methods of the class as long as you have declared the class to use in the first row of the table.
The next step is to call the method addInventories(). Now imagine we have no idea what fields we need to populate this method. Let's go to the code to see what we need. Here is the code from CatalogSetUpFixture.java:
/**
* Action method <code>add inventories</code>.
*
* @return <code>AddInventories</code> SetUpFixture
*/
public SetUpFixture addInventories() {
return new AddInventories();
}
Because we know that addInventories() does not take in any parameters, all we need on the second row is just one cell, namely "add inventories". Here is what the table looks like at this point:
| use | com.elasticpath.fit.setup.CatalogSetUpFixture | |||
| add inventories | ||||
Still with me? Good. This part is going to get a little confusing, so pay attention. The addInventories() method returns a class of type Fixture, we can infer that there will be more rows in this table. But how do the row of tables look like? In order to answer this question, we have to go to the class AddInventories. In this class, we get the variables through a special method, whose name is a concatenation of the parameters it is expecting. As you can see from the following code, this looks pretty strange.
/**
* SetUpFixture to add inventories.
*/
public class AddInventories extends SetUpFixture {
/**
* Action method <code>product sku code warehouse code qty on hand reserved qty allocated qty</code>.
*
* @param skuCode sku code
* @param warehouseCode warehouse code
* @param qtyOnHand qty on hand
* @param reservedQty reserved qty
* @param allocatedQty allocated qty
*/
public void productSkuCodeWarehouseCodeQtyOnHandReservedQtyAllocatedQty(final String skuCode, final String warehouseCode,
final int qtyOnHand, final int reservedQty, final int allocatedQty) {
Warehouse warehouse = warehouseService.findByCode(warehouseCode);
catalogPersister.persistInventory(skuCode, warehouse, qtyOnHand, reservedQty, allocatedQty);
}
/**
* Action method <code>product sku code warehouse code qty on hand reserved qty allocated qty reorder minimum reorder qty</code>.
*
* @param skuCode sku code
* @param warehouseCode warehouse code
* @param qtyOnHand qty on hand
* @param reservedQty reserved qty
* @param allocatedQty allocated qty
* @param reorderMinimum reorder minimum
* @param reorderQty reorder qty
*/
public void productSkuCodeWarehouseCodeQtyOnHandReservedQtyAllocatedQtyReorderMinimumReorderQty(final String skuCode,
final String warehouseCode, final int qtyOnHand, final int reservedQty, final int allocatedQty, final int reorderMinimum,
final int reorderQty) {
Warehouse warehouse = warehouseService.findByCode(warehouseCode);
catalogPersister.persistInventory(skuCode, warehouse, qtyOnHand, reservedQty, allocatedQty, reorderMinimum, reorderQty);
}
}
For this part, we need to separate that long method name into variable names. The order of the names must be the same as the order they appear in the method name. After this, the table should look like the following:
| use | com.elasticpath.fit.setup.CatalogSetUpFixture | |||
| add inventories | ||||
| product sku code | warehouse code | qty on hand | reserved qty | allocated qty |
After we have all the variables split in to the correct format, we can add the data. We can add as many data rows as we want. Finally, table should look similar to the following:
| use | com.elasticpath.fit.setup.CatalogSetUpFixture | |||
| add inventories | ||||
| product sku code | warehouse code | qty on hand | reserved qty | allocated qty |
| PUCK1 | Sports Warehouse | 0 | 3 | 0 |
The convention here is to color variables blue so that they stand out. The color properties are ignored by the FIT framework, so it is encouraged that you use colors for variables.
Validation
Now that we have added all the data neccessary for this test, we have to test whether or not the operation was successful. Here is the following table that does the trick.
| inventory summary product sku code | PUCK1 | warehouse code | Sports Warehouse | |||
Time for a quick quiz. What is the expected method name that we should see in PaymentServiceFixture.java? If you guessed inventorySummaryProductSkuCodeWarehouseCode(String, String), you're absolutely correct! This is the code in PaymentServiceFixture.java:
/**
* Checks inventory summary information.
*
* @param skuCode product SKU code
* @param warehouseCode warehouse code
* @return <code>ParamRowFixture</code> parameterized with <code>InventoryDetailsRow</code> objects
* @see <code>InventoryDetailsRow</code>
*/
public Fixture inventorySummaryProductSkuCodeWarehouseCode(final String skuCode, final String warehouseCode) {
ProductSku productSku = productSkuService.findBySkuCode(skuCode);
Warehouse warehouse = warehouseService.findByCode(warehouseCode);
Inventory inventory = productSku.getInventory(warehouse.getUidPk());
return new ParamRowFixture(new Object[] { new InventoryDetailsRow(inventory) });
}
The method returns a Fixture, which means another row in the table is required. But what does the next row look like? To find out, we need to look at the code into InventoryDetailsRow. We wrap the information into a row object. This is required by the FIT framework. Let's take a look at InventoryDetailsRow.java:
package com.elasticpath.fit.rowdata;
import com.elasticpath.domain.catalog.Inventory;
/**
* Used in FIT tests as row representation of inventory details.
*/
public class InventoryDetailsRow {
/**
* Quantity on hand.
* FIT field: qty on hand.
*/
public int qtyOnHand;
/**
* Reserved quantity.
* FIT field: reserved qty.
*/
public int reservedQty;
/** allocated quantity. */
public int allocatedQty;
/** available qty in stock. */
public int availableQtyInStock;
/**
* Constructor fills the inventory details.
*
* @param inventory the inventory
*/
public InventoryDetailsRow(final Inventory inventory) {
qtyOnHand = inventory.getQuantityOnHand();
reservedQty = inventory.getReservedQuantity();
allocatedQty = inventory.getAllocatedQuantity();
availableQtyInStock = inventory.getAvailableQuantityInStock();
}
}
The public fields in this class are the fields that we need in the FIT tables. They will determine the column names for the next row. In this case there are 4 fields: qtyOnHand, reservedQty, allocatedQty, and availableQtyInStock. We then add another row, which will contain the values that we expect the test to have. These fields also follow the concatenation convention, so you can either use the exact name of the field, or put spaces between each of the uppercase characters and use a lowercase letter instead. Notice that the backend for creating data and checking data is a bit different, even though in our FIT tables, it looks similar. This is because when we are checking data, FIT needs to validate the values in the table against the system, and it does so with row objects in this case. Finally, our last table looks like this:
| inventory summary product sku code | PUCK1 | warehouse code | Sports Warehouse | |||
| qty on hand | reserved qty | allocated qty | available qty in stock | |||
| 22 | 3 | 0 | 19 | |||
If we run the test, this is what we will see in our results page:
| Click on picture to enlarge |
|---|
![]() |
FIT is an invaluable testing tool for ensuring code quality and stability for the Product Development team. In addition to providing us with component tests, it ensures a measure of stability with our daily builds. After every build, our server runs all FIT tests in the system to make sure recent code commits don't cause any of the tests to fail. In that way, it acts as a source for regression testing. We've also recently started using FIT for acceptance testing. Since testers don't need to write code, they can write the tests (tables) in advance. Developers would then implement the fixture code after they have developed a new feature. A new feature is not complete until it passes all the FIT tests in that area. Finally, we are realizing other uses for FIT, including:
- Sample data population (using scenario data to build sample data, including catalogs, products, customers)
- Performance testing (creating batches of data and running the application under JProfiler to measure memory usage, CPU time, etc.)
Stay tuned for more detailed explanation on these two topics in another blog post!
