top of page
Search

Test driven development with HCL Commerce

  • Jędrzej Małkiewicz
  • Jun 30, 2024
  • 6 min read

Updated: Oct 23

In this article I’ll share with you how to approach test driven development (TDD) with HCL Commerce.

Just to remind test driven development is a way of creating code by starting with writing a test, making sure it fails and writing a piece of logic that will satisfy the test. Then you repeat that cycle until all use cases are covered (and tested). If you want to find out more on what test driven is there are plenty of articles available online.


Requirement


I’ll show you how to use TDD to implement a hypothetical requirement. The requirement was to create an API that returns a list of brands available in the store. The list needed to include: brand identifier, image, name. A brand might appear only once in the results and had to be sorted by name when returned by the API (but could be unordered in the source of data). The API was supposed to be consumed by UI developers. The product owner wanted to have an MVP quickly and did not know where and how brands would be managed. We also agreed the information could be simply stored in a file as it’d not change very often.


I analyzed the story and I decided that I was going to create a new GET {storeId}/brands API that would return an array of brands

{
“brand”: 	  
[
  {
   “id”:”brand1”,
   ”imageUrl” :”https://server.sample/brand1.jpeg”,
   ”name”:”Brand One”
  },
  {
   “id”: ”brand2”, 
   ”imageUrl”: ”https://server.sample/brand1.jpeg”,
   ”name”: ”Brand Two”}]
}

There was enough information in the story to start planning development. I was tempted to immediately jump into coding handlers, beans or commands, but I had to hold on for a second and come up with some tests first.


Based on the requirement I found few obvious candidates for my test cases:

  1. Get HTTP 200 for a call to {storeId}/brands

  2. A successful response should contain a “brands” element that will hold an array of brands

  3. Each brand element in the brand array should contain “id”, “imageUrl”, “name” attributes.

  4. Values in the API response need to match the values from the file. For example store 1 there will be a brand with id=brand1, imageUrl= https://server.sample/brand1.jpeg and name=“Brand One”

  5. Brands should to be sorted by name

  6. A brand should only appear once in the result


In order to create my tests I used a combination of JUnit 5 and REST-assured.


The first set of tests


I started with tests number 1), 2), 3) and created integration tests meaning they’d require the ts-app server to run.


@DisplayName("/brands")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BrandsRestHandlerTest {


    @BeforeAll
    public static void setUp() {
        RestAssured.baseURI = "https://localhost:5443";
        RestAssured.config = RestAssuredConfig.config().logConfig(LogConfig.logConfig().enablePrettyPrinting(true));
        RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter());
        RestAssured.useRelaxedHTTPSValidation();
    }

    @Test
    @Order(1)
    @DisplayName("GET should return http 200")
    public void should_return_http_200(){
        when().get("/wcs/resources/store/11/brands").then().statusCode(200);
    }

    @Test
    @Order(2)
    @DisplayName("GET should contain an array of brands")
    public void should_contain_an_array_of_brands(){
        when().get("/wcs/resources/store/11/brands").then().assertThat().statusCode(200).and().body("brand", hasSize(greaterThanOrEqualTo(0)));

    }

    @Test
    @Order(3)
    @DisplayName("GET should contain objects with 'id', 'imageUrl', 'name' attributes in brands array")
    public void should_contain_objects_with_id_imageUrl_name_attributes_in_the_of_brands(){
        when().get("/wcs/resources/store/11/brands").then().statusCode(200).and().body("brand",hasItems( hasKey("id"), hasKey("imageUrl"), hasKey("name")));

        ));
    }

}

After running the tests I saw that all of them have failed. Good news!


ree

Add logic that lets the tests pass


It was time to try to create some code in order to make the first green. I created a handler for the /brands endpoint and registered it in the resources-ext.properties file.


@Path("store/{storeId}/brands")
public class BrandsRestHandler extends AbstractConfigBasedClassicHandler {
    private final static String RESOURCE_NAME = "brands";

    @Override
    public String getResourceName() {
        return RESOURCE_NAME;
    }

    @GET
    public Response get(@PathParam("storeId") String storeId){
        return Response.status(200).build();
    }

}

After deploying the changes to the server I ran the test again and looked at the results. As expected the first test passed.

ree

I added the implementation of the second one. At this stage developed a data bean to back my handler. Notice that I didn't yet decide how I’d populate the brands array, I just wanted the test to pass.

public class BrandListDataBean extends SmartDataBeanImpl implements Delegator
 {
    
    private List brands;

    @Override
    public void populate() throws Exception {
        brands = new ArrayList();
    }

    public List getBrands() {
        return brands;
    }

    @Override
    public Protectable getDelegate() throws Exception {
       return null;
    }

}

I also needed to add the Rest mapping for the new data bean:

<?xml version="1.0" encoding="UTF-8"?>
<bean>
    <profiles>
        <profile name="OVIRIO_Summary">
            <outputs>
                <output methodName="getBrands" outputName="brand"/>
            </outputs>
        </profile>
    </profiles>
</bean>

and modified the handler to use the bean:

@GET
public Response get(@PathParam("storeId") String storeId){
     return executeConfigBasedBeanWithContext(BrandListDataBean.class.getName(), DEFAULT_PROFILE_NAME, null,null);
}

I ran my tests again and (naively expecting the second test to pass), but what happened? Not only the second test was failing but also the first one was not green any more.

ree

It was time to look at the logs. I saw the below error:


   {
    "errors": [
        {
            "errorKey": "NOT_AUTHORIZED_FOR_QUERY",
            "errorParameters": [
                "eu.ovirio.commerce.brands.bean.BrandListDataBean"
            ],
            "errorMessage": "CWXFR0268E: You are not authorized to execute query: GET https://localhost:5443/wcs/resources/store/11/brands",
            "errorCode": "CWXFR0268E"
        }
    ]
}

Ok. Seems like the I forgot to configure and access control policy for the bean. After creating an ACP for the new bean to allow guests users to access and loading it I saw that 2 tests were green. I was back on track!

ree

For the 3rd test I changed BrandListDataBean to return a list of Brand objects (a class with id, imageUrl and name properties).

private List<Brand> brands;

@Override
public void populate() throws Exception {
    brands = new ArrayList(1);
    brands.add(new Brand("1","https://something","B"));
}

public List<Brand> getBrands() {
    return brands;
}

I also had to update the bean mapping configuration file.

<outputs>
    <output methodName="getBrands" outputName="brand">
        <output methodName="getId" outputName="id"/>
        <output methodName="getImageUrl" outputName="imageUrl"/>
        <output methodName="getName" outputName="name"/>
    </output>
</outputs>

I reran the tests and all 3 were OK.

ree

The second set of tests

Next I created 3 remaining test cases meaning 4), 5) & 6). Unlike the first 3 tests these tests did not require a sever to be up & running in order to unit test the BrandListDataBean Java class.


I created 4 test cases and initially all of them failed.


@DisplayName("BrandListDataBean")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class BrandListDataBeanTest {

    @Test
    @Order(1)
    @DisplayName("should return correct results for a store")
    public void should_return_correct_results_for_store_1() throws Exception {
        BrandListDataBean bean = new BrandListDataBean("src/test/resources/test1.json");
        bean.setStoreId(1);
        bean.populate();
        List<Brand> brands = bean.getBrands();
        Brand brand = brands.get(0);
        assertEquals(1,brands.size());
        assertEquals("Brand One", brand.getName());
        assertEquals("https://server.sample/brand1.jpeg", brand.getImageUrl());
        assertEquals("brand1", brand.getId());
    };

    @Test
    @Order(2)
    @DisplayName("should not change the order if the source system returns brands in the correct order")
    public void should_not_change_order_if_source_system_has_sorted_list() throws Exception {
        BrandListDataBean bean = new BrandListDataBean("src/test/resources/test2.json");
        bean.setStoreId(1);
        bean.populate();
        List<Brand> brands = bean.getBrands();
        assertEquals("Brand One", brands.get(0).getName());
        assertEquals("Brand Two", brands.get(1).getName());
    };


    @Test
    @Order(3)
    @DisplayName("should change the order if the source system returns brands in the incorrect order")
    public void should_change_order_if_source_system_has_sorted_list() throws Exception {
        BrandListDataBean bean = new BrandListDataBean("src/test/resources/test3.json");
        bean.setStoreId(1);
        bean.populate();
        List<Brand> brands = bean.getBrands();
        assertEquals("Brand One", brands.get(0).getName());
        assertEquals("Brand Two", brands.get(1).getName());
    };

    @Test
    @Order(4)
    @DisplayName("should remove duplicates if the source system returns the same brand multiple times")
    public void should_return_the_same_brand_only_once_if_source_contains_the_same_brand_more_then_once() throws Exception {
        BrandListDataBean bean = new BrandListDataBean("src/test/resources/test4.json");
        bean.setStoreId(1);
        bean.populate();
        List<Brand> brands = bean.getBrands();
        assertEquals("Brand One", brands.get(0).getName());
        assertEquals(1,brands.size());
    };

}

At this point when I ran the tests I expected some compilation errors (I needed to add the constructor and some code to read values from the file).

The structure of the file where I kept brands looked like that:

[
  {
    "id": "brand1",
    "name": "Brand One",
    "imageUrl": "https://server.sample/brand1.jpeg",
    "storeId": 1
  },
  {
    "id": "brand1",
    "name": "Brand One",
    "imageUrl": "https://server.sample/brand1.jpeg",
    "storeId": 2
  }
]

Adding next batches of business logic


I added just enough of code to pass the 1st test case for the data bean.

private int storeId;

private String brandsFileName = "brands.json";

BrandListDataBean(String fileName){
    this.brandsFileName = fileName;
}

…

@Override
public void populate() throws Exception {

    brands = new ArrayList<>(0);

    try {

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
        Brand [] brandsArray = mapper.readValue((InputStream) new FileInputStream(getBrandsFileName()), Brand[].class);
        if (brandsArray != null) {
            Collections.addAll(brands, brandsArray);
        }
        else
             return;

        brands = brands.stream().filter((brand->brand.getStoreId()==getStoreId())).collect(Collectors.toList());

    } catch(Exception e) {
        e.printStackTrace(); 
    }
}

private  String getBrandsFileName() {
    return brandsFileName;
}

Adding the code has covered 2 cases (that is why I decided to include the extra test case for sorting).

ree

Next I had to add support for sorting in the data bean.

brands.sort(Comparator.comparing(Brand::getName);

Reran tests... and was one test from the finish line.

ree

What had left was to add support for including distinct brand ids only to the data bean.

brands = brands.stream().filter((brand->brand.getStoreId()==getStoreId())).filter(distinctByKey(Brand::getId)).collect(Collectors.toList());

I executed tests one more time and now I saw all tests of the data bean turned green.

ree

I was about to celebrate but then I saw that all the tests I created for the rest handler were failing! I implemented one thing and I broke another! Thanks to TDD I was aware of it and I knew I had to fix it.


ree

Turned out I forgot about 3 things:

  • adding a default constructor to the data bean

  • mapping storeId parameter in the Rest API data bean mapping file

  • pointing a data bean to a json file with brands inside of the EAR file

I added missing code and configuration and now all tests got green.


ree

and the API finally worked as expected:

ree

Summary

As you can see in the article applying TDD with HCL Commerce does not have to be difficult and it helped me few times to discover defects while working on a feature.

If your team struggles with unit testing or plans to bring testability to a next level by introducing TDD to your HCL Commerce do not hesitate to contact us we can help you with that.

 
 

Recent Posts

See All
bottom of page