Skip to content

Commit

Permalink
Task1 - Implement service and acceptance tests
Browse files Browse the repository at this point in the history
- Some AT refactor & fixes
- User controller fix for kafka event sending
- Changes on the PurchaseEntity
- Changes on the service side listener for PurchasEvents
  • Loading branch information
PietroSassone committed May 5, 2022
1 parent 5685b0e commit dc4ff1d
Show file tree
Hide file tree
Showing 17 changed files with 195 additions and 49 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ The goal of this project is to give a demo about acceptance test implementation
In order to do so, there's a simple Spring Boot REST API implementation included in the project.
The service has a local H2 DB connection, which is automatically set upd and started along with starting the service.
The acceptance tests use Jersey REST client for calling the controllers of the service.
At the moment there are no unit tests, as I wanted to demonstrate higher level testing. They may be added later.
At the moment there are no unit tests, as I wanted to demonstrate higher level testing. They may be added later.

The service supports CRUD operations for Users and Products via web controllers. Also stores Purchase info received from Kafka.
These entities have a limited dataset. In a real project they would store more data.
Like currency and such. This is just a simple demo with limited data.

**1. Pre-requirements for running the application**
- Have Maven installed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.WebTarget;

Expand Down Expand Up @@ -64,4 +65,9 @@ public DataSource dbDataSource() {
public JdbcTemplate jdbcTemplate(final DataSource dbDataSource) {
return new JdbcTemplate(dbDataSource);
}

@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import io.cucumber.java.After;
import io.cucumber.java.Before;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class KafkaEventStepdefs extends BaseSteps {
private static final Duration TEN_SECONDS = Duration.ofSeconds(10);
private static final String TEST_DATA_FOLDER = "kafka";
private static final String PURCHASE_EVENT_JSON = "purchaseEvent.json";

private TopicPartition userEventTopicPartition;
private TopicPartition purchaseEventTopicPartition;
private String purchaseEventJsonAsString;
private List<UserOperationNotificationEvent> userEventsFromKafka = new ArrayList<>();

Expand Down Expand Up @@ -65,18 +70,34 @@ public class KafkaEventStepdefs extends BaseSteps {
private KafkaEventDeserializer eventDeserializer;

@Before("@KafkaUserEvent")
public void beforeTest() {
public void beforeKafkaUserEventTest() {
userEventTopicPartition = new TopicPartition(userTopicName, 0);
userEventKafkaConsumer.assign(Collections.singletonList(userEventTopicPartition));
}

@Before("@KafkaPurchaseEvent")
public void beforeKafkaPurchaseEventTest() {
purchaseEventTopicPartition = new TopicPartition(purchaseTopicName, 0);
}

@After("@KafkaUserEvent")
public void afterTest() {
public void afterKafkaUserEventTest() {
kafkaTopicUtil.purgeKafkaTopic(userEventTopicPartition);
}

@After("@KafkaPurchaseEvent")
public void afterKafkaPurchaseEventTest() {
kafkaTopicUtil.purgeKafkaTopic(purchaseEventTopicPartition);
}

@Given("a purchase event is prepared")
public void aPurchaseEventIsPrepared() {
purchaseEventJsonAsString = fileReader.readFileToString(PURCHASE_EVENT_JSON, TEST_DATA_FOLDER);
}

@When("the purchase event is sent to Kafka")
public void thePurchaseEventIsSentToKafka() {
log.info("attempting to send event to kafka: {}", purchaseEventJsonAsString);
purchaseEventKafkaTemplate.send(purchaseTopicName, deserializePurchaseEvents(purchaseEventJsonAsString));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import jakarta.ws.rs.core.Response;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class GetUserStepDefs extends BaseSteps {

private static final String GET_USER_ENDPOINT_PATH = "/api/user/%s/getUser";
private static final String TEST_DATA_INSERT_FILENAME = "insertUserSql.sql";
private static final String SPACE = " ";
private static final String SPACE_HEXA = "%20";

private String userName;
private String formattedEndpointPath;
private Response response;
private Long existingUserId;

@Autowired
Expand Down Expand Up @@ -76,7 +75,7 @@ public void aUserIsSavedInTheDatabase(final String userName, final double balanc

@And("^the username parameter for the request is set to (.*)$")
public void theUserNameIsSet(final String userNameToSet) {
userName = keepStringOrSetToNull(userNameToSet);
final String userName = keepStringOrSetToNull(userNameToSet);
testDataRepository.setUserName(userName);

formattedEndpointPath = String.format(GET_USER_ENDPOINT_PATH, userName);
Expand All @@ -85,8 +84,7 @@ public void theUserNameIsSet(final String userNameToSet) {

@When("the getUser endpoint is called")
public void callTheGetUserEndpoint() {
response = requestUtil.executeGetRequest(formattedEndpointPath);
testDataRepository.setResponse(response);
testDataRepository.setResponse(requestUtil.executeGetRequest(formattedEndpointPath));
}

@Then("the response body should contain the correct user info")
Expand All @@ -97,7 +95,7 @@ public void theResponseShouldContainTheExpectedUser() throws JSONException {
expectedResponseJson.put(USER_NAME_NODE_NAME, testDataRepository.getUserName());
expectedResponseJson.put(BALANCE_NODE_NAME, testDataRepository.getUserBalance());

jsonHelper.setRestResponseLink(expectedResponseJson, testDataRepository.getResourceSelfLink());
jsonHelper.setRestResponseLink(expectedResponseJson, testDataRepository.getResourceSelfLink().replaceAll(SPACE, SPACE_HEXA));

JSONAssert.assertEquals(expectedResponseJson.toString(), testDataRepository.getResponse().readEntity(String.class), JSONCompareMode.LENIENT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
public class UpdateUserStepDefs extends BaseSteps {
private static final String UPDATE_USER_ENDPOINT_PATH = "/api/user/users/%s";
private static final String CHANGE_REASON_NODE_NAME = "changeReason";
private static final String SELF_LINK_TEMPLATE = "/api/user/%s/getUser";

private String userChangeReason;

Expand All @@ -31,6 +32,7 @@ public void callTheUpdateUserEndpoint() {
final ObjectNode request = prepareUserRequestBody(testDataRepository.getRequestAsJson());

request.put(CHANGE_REASON_NODE_NAME, userChangeReason);
testDataRepository.setResourceSelfLink(String.format(SELF_LINK_TEMPLATE, testDataRepository.getUserName()));

testDataRepository.setResponse(
requestUtil.executePutRequest(String.format(UPDATE_USER_ENDPOINT_PATH, testDataRepository.getUserId()), String.valueOf(request))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.nio.file.Files;
import java.nio.file.Paths;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
Expand All @@ -16,9 +17,12 @@ public class FileReaderUtil {
private static final String FILE_PATH_TEMPLATE = "testdata/%s/%s";
private static final String DELIMITER = "\n";

@Autowired
private ObjectMapper objectMapper;

public ObjectNode readFileToJsonNode(final String file, final String locationFolder) {
try (final InputStream fileInputStream = this.getClass().getClassLoader().getResourceAsStream(String.format(FILE_PATH_TEMPLATE, locationFolder, file))) {
return (ObjectNode) new ObjectMapper().readTree(fileInputStream);
return (ObjectNode) objectMapper.readTree(fileInputStream);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.demo.acceptance.tests.util;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.demo.service.events.PurchaseEvent;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

@Component
public class KafkaEventDeserializer {

private static final TypeReference<PurchaseEvent> EVENT_TYPE_REFERENCE = new TypeReference<>() {
};

@Autowired
private ObjectMapper objectMapper;

public PurchaseEvent deserializeJsonToPurchaseEvent(final String eventJsonAsString) {
PurchaseEvent deserializedEvent;
try {
deserializedEvent = objectMapper.readValue(eventJsonAsString, EVENT_TYPE_REFERENCE);
} catch (IOException exception) {
throw new RuntimeException(exception);
}
return deserializedEvent;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@updateUser @createUser @KafkaUserEvent @test
@updateUser @createUser @KafkaUserEvent
Feature: Demo service update user endpoint test scenarios
Testing the update user endpoint
Endpoint: POST /api/user/users/{id}
Expand Down Expand Up @@ -27,7 +27,6 @@ Testing the update user endpoint
| Summer | 0.0 |
| exactly 20 chars lon | 1000000000000.0 |

@KafkaUserEvent
Scenario: The endpoint should update user if it already exists with the given id
Given a user exists in the database with name Gandalf the Grey and a balance of 100000
And the userName value for the request is set to <newUserName>
Expand Down
18 changes: 11 additions & 7 deletions demo_svc/src/main/java/com/demo/service/events/PurchaseEvent.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package com.demo.service.events;

import java.util.Map;
import java.util.List;

import com.demo.web.entity.ProductEntity;
import com.demo.service.model.PurchaseDetail;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@AllArgsConstructor
@ToString
@Builder
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class PurchaseEvent {

private final long eventId;
private final long userId;
private final Map<ProductEntity, Integer> productsWithQuantities;
private final Double totalValue;
private long eventId;
private long userId;
private List<PurchaseDetail> purchaseDetails;
private Double totalValue;
}
15 changes: 15 additions & 0 deletions demo_svc/src/main/java/com/demo/service/model/PurchaseDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.demo.service.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@ToString
@NoArgsConstructor
public class PurchaseDetail {
private String productId;
private String productName;
private Double price;
private int quantity;
}
26 changes: 0 additions & 26 deletions demo_svc/src/main/java/com/demo/service/model/PurchaseModel.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.demo.service.service;

import com.demo.web.entity.PurchaseEntity;

public interface PurchaseService {

PurchaseEntity savePurchase(PurchaseEntity purchase);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.demo.service.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.demo.service.service.PurchaseService;
import com.demo.web.entity.PurchaseEntity;
import com.demo.web.repository.PurchaseRepository;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class PurchaseServiceImpl implements PurchaseService {

@Autowired
private PurchaseRepository purchaseRepository;

@Override
public PurchaseEntity savePurchase(final PurchaseEntity purchase) {
log.info("Saving the purchase event content to the database.");
return purchaseRepository.save(purchase);
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,69 @@
package com.demo.service.service.kafka;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import com.demo.service.events.PurchaseEvent;
import com.demo.service.exception.ProductNotFoundException;
import com.demo.service.exception.UserNotFoundException;
import com.demo.service.service.ProductService;
import com.demo.service.service.PurchaseService;
import com.demo.service.service.UserService;
import com.demo.web.entity.ProductEntity;
import com.demo.web.entity.PurchaseEntity;
import com.demo.web.entity.UserEntity;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class KafkaEventListener {

@Autowired
private PurchaseService purchaseService;

@Autowired
private UserService userService;

@Autowired
private ProductService productService;

private final CountDownLatch purchaseEventLatch = new CountDownLatch(1);

@KafkaListener(topics = "${purchase.topic.name}", containerFactory = "purchaseEventKafkaListenerContainerFactory")
public void purchaseEventListener(final PurchaseEvent purchaseEvent) {
log.info("Received purchaseEvent message: {}", purchaseEvent);
this.purchaseEventLatch.countDown();

final UserEntity userFromThePurchase = userService.findByUserId(purchaseEvent.getUserId())
.orElseThrow(() -> new UserNotFoundException(purchaseEvent.getUserId()));

final List<ProductEntity> productsFromThePurchase = new ArrayList<>();
final Map<Long, Integer> productIdsWithQuantities = new HashMap<>();

purchaseEvent.getPurchaseDetails().forEach(details -> {
final Long productId = Long.parseLong(details.getProductId());
productsFromThePurchase.add(
productService.findByProductId(productId).orElseThrow(() -> new ProductNotFoundException(productId))
);

productIdsWithQuantities.put(productId, details.getQuantity());
});

final PurchaseEntity receivedPurchase = PurchaseEntity.builder()
.id(purchaseEvent.getEventId())
.user(userFromThePurchase)
.productEntities(productsFromThePurchase)
.productIdsWithQuantities(productIdsWithQuantities)
.totalValue(purchaseEvent.getTotalValue())
.build();

purchaseService.savePurchase(receivedPurchase);
}
}
Loading

0 comments on commit dc4ff1d

Please sign in to comment.