Microservice Architecture notes follow:
1. A stock market example. | 4. Implementation. |
2. Solution with Microservice. | 5. Testing with Microservice. |
3. Apply Microservices with Spring. | 6. References. |
1. A stock market example
- In the stock market, settlement is all processes the buyer received his shares and the seller received his money after they bought or sold shares through his broker.
- The period within which buyers receive their shares and sellers receive their money is called a settlement cycle.
This period depends on the settlement cycle in different market. For instance, ASX Ltd (Australian Securities Exchange) used a T+2 settlement cycle for cash market trades in Australia. This means if a seller sells shares, his broker will receive money on the second working day.
Suppose that there are transactions clients have made in a settlement cycle presented by the Securities and Exchange Board:
TYPE STOCK-VALUE COMMISSION TAX CLIENT BUY 100 MSFT@20 2000 5 CLSA (Credit Lyonnais Securities Asia) SELL 100 MSFT@20 2100 5.5 CLSA (Credit Lyonnais Securities Asia)
Target is to build an application for illustrating trading transactions as on the Board.
2. Solution
2.1 Microservice architecture
Figure 1: Microservice architecture (source: martinfowler.com)
2.2 System architecture
Figure 2: Multi-services work together as a system to provide business features
- Logic/module boundary contains service B which presented an id generator service for transactions made.
- Logic/module boundary including service A will be transaction processes involved calling the api of service B.
- Front-end will ask for a JSON representation of a transaction. REpresentational State Transfer (REST) used over HTTP protocol in the system.
3. Apply microservices with Spring.
3.1. Resource component.
According to Martinfowler,”Resources act as mappers between the application protocol exposed by the service and messages to objects representing the domain. Typically, they are thin, with responsibility for sanity checking the request and providing a protocol specific response according to the outcome of the business transaction.”. The figure below shows how to implement resource in a specific system.
Figure 3: Transaction resource Layer.
3.2. Domain components.
“Almost all of the service logic resides in a domain model representing the business domain. Of these objects, services coordinate across multiple domain activities, whilst repositories act on collections of domain entities and are often persistence backed.”
Figure 4: Transaction Domain Layer.
3.3. External components.
“If one service has another service as a collaborator, some logic is needed to communicate with the external service. A gateway encapsulates message passing with a remote service, marshalling requests and responses from and to domain objects. It will likely use a client that understands the underlying protocol to handle the request-response cycle.”
Figure 5: Transaction External Layer.
3.4. Data components.
“Except in the most trivial cases or when a service acts as an aggregator across resources owned by other services, a micro-service will need to be able to persist objects from the domain between requests. Usually this is achieved using object relation mapping or more lightweight data mappers depending on the complexity of the persistence requirements.
Often, this logic is encapsulated in a set of dedicated objects utilised by repositories from the domain.”
Figure 6: Transaction Persistance Layer.
4. Implementation.
As Figure 2, the two module boundaries named as settlement-transaction-idgenerator and settlement-transaction-management. The former provides a API that automatically generates a transaction ID combined by market code, comapany symbol in stock market and a sequence number. The latter handles transaction processes such as save, list, and get a particular transaction.
4.1. settlement-transaction-management module.
Figure 5: settlement-transaction-management structure.
Resource component code example.
src/main/java/com/mycompany/app/transaction/api/TransactionResource.java
package com.mycompany.app.transaction.api; import com.mycompany.app.transaction.utils.Pagination; import org.springframework.http.ResponseEntity; import com.mycompany.app.transaction.api.dto.TransactionDTO; public interface TransactionResource { /** * @api {POST} /api/transaction/add Add a new transaction. * @apiName addTransaction * @apiGroup transaction * @apiDescription Add a new transaction **@apiParamExample {json} Request-Example: * { * "transactionCode": null, * "companySymbol":"TSLA", * "marketCode": "NY", * "transactionType": "SELL", * "stockValue": "@501", * "commission": 100.5, * "tax": 5.5, * "client": "TESLA Technology", * "createdOn":null * } * @apiSuccessExample Success-Response: * * HTTP/1.1 200 OK * { * "transactionCode": "NYSE1100022", * "companySymbol": "TSLA", * "marketCode": "NY", * "transactionType": "SELL", * "stockValue": "@501", * "commission": 100.5, * "tax": 5.5, * "client": "TESLA Technology", * "createdOn": "2016-10-29 19:58:30" * } * * * @apiError TRANSACTION_ID_NOT_FOUND Could not find mapping for entity * @apiErrorExample Error-Response: * HTTP/1.1 404 Not Found * { * "errorCode":"TRANSACTION_ID_NOT_FOUND", * "errorMessage":"Transaction id is not generated"} * } * */ ResponseEntity<Void> saveTransaction(TransactionDTO transactionDTO); Pagination<TransactionDTO> getTransactionList(Integer page, Integer pageSize); TransactionDTO findTransaction(String companySymbol,String transactionCode); }
src/main/java/com/mycompany/app/transaction/api/TransactionResourceImpl.java
package com.mycompany.app.transaction.rest; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import com.mycompany.app.transaction.api.dto.TransactionDTO; import com.mycompany.app.transaction.service.TransactionService; import com.mycompany.app.transaction.utils.Pagination; import com.mycompany.app.transaction.utils.RestException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import com.mycompany.app.transaction.api.TransactionResource; @RestController @RequestMapping("/api") public class TransactionResourceImpl implements TransactionResource { private static final String APPLICATION_JSON_DEFAULT_CHARSET = APPLICATION_JSON_VALUE + ";charset=UTF-8"; @Autowired private TransactionService transactionService; @Override @RequestMapping(value = "/transaction/add", method = RequestMethod.POST, produces = APPLICATION_JSON_DEFAULT_CHARSET) public ResponseEntity<Void> saveTransaction( @RequestBody TransactionDTO transactionDTO) { try{ transactionService.saveTransaction(transactionDTO); return new ResponseEntity<>(HttpStatus.OK); } catch (RestException e) { throw new RestException(e.getRestError(), e.getErrorParams()); } } @Override @RequestMapping(value = "transaction/{companySymbol}/{transactionCode}", method = RequestMethod.GET, produces = APPLICATION_JSON_DEFAULT_CHARSET) @ResponseStatus(HttpStatus.OK) @ResponseBody public TransactionDTO findTransaction(@PathVariable String companySymbol, @PathVariable String transactionCode) { return transactionService .findByTransactionCodeAndCompanySymbol(companySymbol, transactionCode); } @Override @RequestMapping(value = "/transaction/list", method = RequestMethod.GET, produces = APPLICATION_JSON_DEFAULT_CHARSET) @ResponseStatus(HttpStatus.OK) @ResponseBody public Pagination<TransactionDTO> getTransactionList( @RequestParam(value = "page") Integer page, @RequestParam(value = "pageSize") Integer pageSize) { return transactionService.getAllTransactionsByPaging(page, pageSize); } }
Domain component code example.
src/main/java/com/mycompany/app/transaction/api/dto/TransactionDTO.java
package com.mycompany.app.transaction.api.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.mycompany.app.transaction.domain.TransactionType; import com.mycompany.app.transaction.utils.CustomDateSerializer; import org.joda.time.DateTime; @JsonIgnoreProperties(ignoreUnknown = true) public class TransactionDTO extends DTO{ private String transactionCode; private String companySymbol; private String marketCode; private TransactionType transactionType; private String stockValue; private Double commission; private Double tax; private String client; @JsonSerialize(using = CustomDateSerializer.class) private DateTime createdOn; //Setters and Getters
src/main/java/com/mycompany/app/transaction/service/TransactionService.java
package com.mycompany.app.transaction.service; import com.mycompany.app.transaction.api.dto.TransactionDTO; import com.mycompany.app.transaction.utils.Pagination; public interface TransactionService { TransactionDTO saveTransaction(TransactionDTO transactionDTO); Pagination<TransactionDTO> getAllTransactionsByPaging(int page, int pageSize); TransactionDTO findByTransactionCodeAndCompanySymbol(String companySymbol, String transactionCode); }
src/main/java/com/mycompany/app/transaction/service/TransactionServiceImpl.java
package com.mycompany.app.transaction.service; import com.mycompany.app.transaction.api.dto.TransactionDTO; import com.mycompany.app.transaction.domain.Transaction; import com.mycompany.app.transaction.gateway.IDGeneratorGateway; import com.mycompany.app.transaction.gateway.dto.IdCodeDTO; import com.mycompany.app.transaction.repository.TransactionRepository; import com.mycompany.app.transaction.service.mapper.TransactionMapper; import com.mycompany.app.transaction.utils.Pagination; import com.mycompany.app.transaction.utils.RestError; import com.mycompany.app.transaction.utils.RestException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import static org.springframework.data.domain.Sort.Direction.DESC; @Service @Transactional public class TransactionServiceImpl implements TransactionService { private static final Logger LOGGER = LoggerFactory.getLogger(TransactionServiceImpl.class); @Autowired private IDGeneratorGateway idGeneratorGateway; @Autowired private TransactionMapper transactionMapper; @Autowired private TransactionRepository transactionRepository; /**{@inheritDoc} **/ @Override public TransactionDTO saveTransaction(TransactionDTO transactionDTO) { IdCodeDTO idCode = idGeneratorGateway.getIdCode(transactionDTO.getCompanySymbol()); if(StringUtils.isEmpty(idCode.getCode())) { throw new RestException(new RestError("TRANSACTION_ID_NOT_FOUND", "Transaction id is not generated", HttpStatus.NOT_FOUND)); } LOGGER.debug("Save a transaction id {} with symbol {} in marketCode {}", idCode.getCode(), transactionDTO.getCompanySymbol(),transactionDTO.getMarketCode()); transactionDTO.setTransactionCode(idCode.getCode()); Transaction transaction = transactionMapper.toTransaction(transactionDTO); return transactionMapper.toTransactionDTO(transactionRepository.save(transaction)); } /**{@inheritDoc} **/ @Override public TransactionDTO findByTransactionCodeAndCompanySymbol(String companySymbol, String transactionCode) { Transaction transaction = transactionRepository .findByTransactionCodeAndCompanySymbol(companySymbol, transactionCode) .orElseThrow(() -> new RestException(new RestError("TRANSACTION_NOT_FOUND", "Transaction is not found", HttpStatus.NOT_FOUND))); return transactionMapper.toTransactionDTO(transaction); } /**{@inheritDoc} **/ @Override public Pagination<TransactionDTO> getAllTransactionsByPaging(int page, int pageSize) { final PageRequest pageRequest = new PageRequest(page, pageSize, new Sort(new Sort.Order(DESC, "createdOn"))); Page<Transaction> transactionPage = transactionRepository.findAll(pageRequest); return convertToPagination(page, pageSize, transactionPage); } /** Convert current Page to customize Pagination */ private Pagination<TransactionDTO> convertToPagination(int page, int pageSize, Page<Transaction> transactionPage) { Pagination<TransactionDTO> pagination = new Pagination<>(); pagination.setPage(page); Long total = transactionPage.getTotalElements(); pagination.setTotalResult(total); if(total>0) { pagination.setTotalPages(total / pageSize + 1); } else{ pagination.setTotalPages(0L); } pagination.getContent().addAll(transactionMapper.toTransactionDTOList(transactionPage.getContent())); return pagination; } }
External component code example.
src/main/java/com/mycompany/app/transaction/gateway/IDGeneratorGateway.java
package com.mycompany.app.transaction.gateway; import com.mycompany.app.transaction.gateway.dto.IdCodeDTO; public interface IDGeneratorGateway { IdCodeDTO getIdCode(String symbol); }
src/main/java/com/mycompany/app/transaction/gateway/IDGeneratorGatewayImpl.java
package com.mycompany.app.transaction.gateway; import com.mycompany.app.transaction.gateway.dto.IdCodeDTO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.net.URI; @Service public class IDGeneratorGatewayImpl implements IDGeneratorGateway { private final RestTemplate restTemplate; private final String tidGeneratorEndpoint; @Autowired public IDGeneratorGatewayImpl(@Value("${gateway.endpoints.idGenerator.url}") String tidGeneratorEndpoint, RestTemplate restTemplate) { this.restTemplate = restTemplate; this.tidGeneratorEndpoint = tidGeneratorEndpoint; } @Override public IdCodeDTO getIdCode(String symbol) { URI url = UriComponentsBuilder.fromHttpUrl(tidGeneratorEndpoint).queryParam("symbol", symbol).build().toUri(); return restTemplate.getForObject(url, IdCodeDTO.class); } }
Data component code example.
src/main/java/com/mycompany/app/transaction/repository/TransactionRepository.java
package com.mycompany.app.transaction.repository; import com.mycompany.app.transaction.domain.Transaction; import com.mycompany.app.transaction.domain.TransactionComposeKey; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.util.Optional; public interface TransactionRepository extends JpaRepository<Transaction, TransactionComposeKey> { @Query(value="from Transaction t where t.transactionCode = :transactionCode " + "and t.companySymbol = :companySymbol") Optional<Transaction> findByTransactionCodeAndCompanySymbol( @Param(value = "companySymbol") String companySymbol, @Param(value = "transactionCode") String transactionCode); }
src/main/java/com/mycompany/app/transaction/domain/Transaction.java
package com.mycompany.app.transaction.domain; import com.mycompany.app.transaction.utils.DomainConstants; import org.hibernate.annotations.Type; import org.joda.time.DateTime; import javax.persistence.*; @Entity @Table(name="transaction") @IdClass(TransactionComposeKey.class) public class Transaction implements DomainConstants { @Id private String transactionCode; @Id private String companySymbol; @Id private String marketCode; @Column(name = "transaction_type") @Enumerated(EnumType.STRING) private TransactionType transactionType; @Column(name = "stock_value") private String stockValue; @Column(name = "commission") private Double commission; @Column(name = "tax") private Double tax; @Column(name = "client") private String client; @Column(name = "created_on", updatable = false) @Type(type = COMMON_DATE_TYPE) private DateTime createdOn = new DateTime(); // Override toString, equal and hashCode
src/main/java/com/mycompany/app/transaction/domain/Transaction.java
package com.mycompany.app.transaction.domain; import javax.persistence.Column; import javax.persistence.Embeddable; import java.io.Serializable; /** * Defines the compound key used by the Transaction entity for persistence */ @Embeddable public class TransactionComposeKey implements Serializable { @Column(name = "transaction_code") private String transactionCode; @Column(name = "company_symbol") private String companySymbol; @Column(name = "market_code") private String marketCode; // For JPA only protected TransactionComposeKey() {} public TransactionComposeKey(String transactionCode, String companySymbol, String marketCode) { this.transactionCode = transactionCode; this.companySymbol = companySymbol; this.marketCode = marketCode; } // Override Equal and hashCode
Utils code example.
src/main/java/com/mycompany/app/transaction/utils/DomainConstants.java
public interface DomainConstants { String COMMON_DATE_TYPE = "org.jadira.usertype.dateandtime.joda.PersistentDateTime"; }
src/main/java/com/mycompany/app/transaction/utils/CustomDateSerializer.java
package com.mycompany.app.transaction.utils; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.io.IOException; /** * Created by Khiem on 10/29/2016. * JSON format joda datetime. Not recommend format at ModelMapper */ public class CustomDateSerializer extends JsonSerializer<DateTime> { private static DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"); @Override public void serialize(DateTime value, JsonGenerator gen, SerializerProvider arg) throws IOException { gen.writeString(formatter.print(value)); } }
Resource configuration.
src/main/resources/application.yml
server: port: 8082 address: 0.0.0.0 spring: datasource: dataSourceClassName: org.postgresql.ds.PGSimpleDataSource test-on-borrow: true test-while-idle: true validation-query: SELECT 1 validation-query-timeout: 30 time-between-eviction-runs-millis: 300000 url: jdbc:postgresql://localhost:5432/settlement_transaction_management databaseName: settlement_transaction_management serverName: username: postgres password: admin jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect database: POSTGRESQL openInView: false show_sql: false generate-ddl: false hibernate: ddl-auto: none naming-strategy: org.hibernate.cfg.EJB3NamingStrategy properties: hibernate.cache.use_second_level_cache: false hibernate.cache.use_query_cache: false hibernate.generate_statistics: false hibernate.cache.region.factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory gateway: configuration: readTimeout: 20000 connectTimeout: 20000 endpoints: idGenerator: url: http://localhost:8081/api/tidgenerator liquibase: change-log: classpath:config/liquibase/db-changelog.xml
src/main/resources/config/liquibase/db-changelog.xml
<?xml version="1.0" encoding="UTF-8"?> <databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd"> <changeSet id="1" author="khiem.truong"> <createTable tableName="transaction"> <column name="transaction_code" type ="varchar(20)"> <constraints nullable="false" /> </column> <column name="company_symbol" type="varchar(10)"> <constraints nullable="false"/> </column> <column name="market_code" type="varchar(10)"> <constraints nullable="false"/> </column> <column name="transaction_type" type="varchar(255)"> <constraints nullable="false"/> </column> <column name="stock_value" type="varchar(255)"/> <column name="commission" type="double"/> <column name="tax" type="double"/> <column name="client" type="varchar(255)"/> <column name="created_on" type="timestamp"/> </createTable> <addPrimaryKey columnNames="transaction_code,company_symbol,market_code,transaction_type" tableName="transaction"/> </changeSet> </databaseChangeLog>
Make the application executable
src/main/java/com/mycompany/app/transaction/Application.java
package com.mycompany.app.transaction; import ch.qos.logback.classic.LoggerContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.web.client.RestTemplate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; @Configuration @ComponentScan(basePackages = {"com.mycompany.app.transaction"}) @EnableAutoConfiguration @EnableJpaRepositories public class Application { @Autowired private Environment env; private static final Logger LOGGER = LoggerFactory.getLogger(Application.class); @PostConstruct public void initApplication() throws IOException { if (env.getActiveProfiles().length == 0) { LOGGER.warn("No Spring profile configured, running with default configuration"); } else { LOGGER.info("Running with Spring profile(s) : {}", env.getActiveProfiles()); } } public static void main(String[] args) { SpringApplication.run(Application.class, args); /* ApplicationContext ctx = SpringApplication.run(Application.class, args); String[] beanNames = ctx.getBeanDefinitionNames(); Arrays.sort(beanNames); for (String beanName : beanNames) { System.out.println(beanName); }*/ } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } @PreDestroy public void shutdownLogback(){ LOGGER.info("Shutting down Logback"); LoggerContext lCtx = (LoggerContext) LoggerFactory.getILoggerFactory(); lCtx.stop(); } }
4.2. settlement-transaction-idgenerator module.
Resource exposes generated transaction ID
src/main/java/com/mycompany/app/idgenerator/rest/TransactionIdGeneratorResourceImpl.java
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.mycompany.app.idgenerator.api.IdGeneratorResource; import com.mycompany.app.idgenerator.api.dto.TransactionIdDTO; import com.mycompany.app.idgenerator.service.TransactionIdService; @RestController @RequestMapping("/api") public class TransactionIdGeneratorResourceImpl implements IdGeneratorResource { @Autowired private TransactionIdService idCodeService; @RequestMapping(value = "tidgenerator", method = RequestMethod.GET, produces = APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) @ResponseBody public TransactionIdDTO generateIdentificationCode( @RequestParam(value = "symbol") String symbol) { return idCodeService.generateIdentificationCode(symbol); } }
Run the applications of the two modules and using any Rest Client to test APIs:
GET http://localhost:8081/api/tidgenerator?symbol=TSLA Success-Response: HTTP/1.1 200 OK { "code": "NYSE1100019" }
POST http://localhost:8082/api/transaction/add { "transactionCode": null, "companySymbol":"TSLA", "marketCode": "NY", "transactionType": "SELL", "stockValue": "@501", "commission": 100.5, "tax": 5.5, "client": "TESLA Technology", "createdOn":null } Success-Response: HTTP/1.1 200 OK { { "transactionCode": "NYSE1100019", "companySymbol":"TSLA", "marketCode": "NY", "transactionType": "SELL", "stockValue": "@501", "commission": 100.5, "tax": 5.5, "client": "TESLA Technology", "createdOn":"2016-10-29 19:58:30" } }
GET http://localhost:8082/api/transaction/TSLA/NYSE1100018 Success-Response: HTTP/1.1 200 OK { "transactionCode": "NYSE1100018", "companySymbol": "TSLA", "marketCode": "NY", "transactionType": "SELL", "stockValue": "@501", "commission": 100.5, "tax": 5.5, "client": "TESLA Technology", "createdOn": "2016-10-29 17:11:32" }
GET http://localhost:8082/api/transaction/list?page=0&pageSize=10 Success-Response: HTTP/1.1 200 OK { "content": [ { "transactionCode": "NYSE1100022", "companySymbol": "TSLA", "marketCode": "NY", "transactionType": "SELL", "stockValue": "@501", "commission": 100.5, "tax": 5.5, "client": "TESLA Technology", "createdOn": "2016-10-29 19:58:30" }, { "transactionCode": "NYSE1100021", "companySymbol": "TSLA", "marketCode": "NY", "transactionType": "SELL", "stockValue": "@501", "commission": 100.5, "tax": 5.5, "client": "TESLA Technology", "createdOn": "2016-10-29 19:58:28" } ], "page": 0, "totalResult": 2, "totalPages": 1 }
Seem it works fine but not enough for a stable system. we need to include testing.
5. Testing with Microservice.
Testing makes sure that all APIs/functions work properly. When someone tries to modify some codes, those changes potentially affect on whole system. Therefore, sufficient testing e.g unit, integration tests..together prevents us from breaking the behavior of the system.
5.1 Testing Services.
To mock a API, Using MockRestServiceServer calls a request for the generate transaction ID.
src/test/java/com/mycompany/app/TransactionServiceITTest.java
package com.mycompany.app; import com.mycompany.app.transaction.Application; import com.mycompany.app.transaction.api.dto.TransactionDTO; import com.mycompany.app.transaction.domain.Transaction; import com.mycompany.app.transaction.domain.TransactionComposeKey; import com.mycompany.app.transaction.domain.TransactionType; import com.mycompany.app.transaction.repository.TransactionRepository; import com.mycompany.app.transaction.service.TransactionService; import com.mycompany.app.transaction.utils.Pagination; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; import java.util.stream.IntStream; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; /** * Created by Khiem on 10/29/2016. */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = {Application.class}) @WebAppConfiguration @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @ActiveProfiles("devmock") public class TransactionServiceITTest { @Autowired private RestTemplate restTemplate; @Autowired private TransactionService transactionService; @Autowired private TransactionRepository transactionRepository; private MockRestServiceServer mockServer; @Before public void setup() { mockServer = MockRestServiceServer.createServer(restTemplate); IntStream.range(1, 3).forEach(i -> { mockServer.expect(requestTo( "http://localhost:8081/api/tidgenerator?symbol=TSLA")) .andExpect(method(HttpMethod.GET)).andRespond( withSuccess().contentType(MediaType.APPLICATION_JSON).body( "{\"code\": \"NYSE110001" + i + "\"}")); }); } @Test public void addTransactionSuccessTest() { try { TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.SELL); transactionDTO.setStockValue("@501"); transactionDTO.setCommission(100.5); transactionDTO.setTax(5.5); transactionDTO.setClient("TESLA Technology"); transactionService.saveTransaction(transactionDTO); TransactionComposeKey key = new TransactionComposeKey("NYSE1100011","TSLA","NY"); Transaction transaction = transactionRepository.findOne(key); Assert.assertNotNull(transaction); Assert.assertEquals("NYSE1100011", transaction.getTransactionCode()); Assert.assertEquals("TSLA", transaction.getCompanySymbol()); Assert.assertEquals("NY", transaction.getMarketCode()); Assert.assertNotNull(transaction.getCreatedOn()); } finally { transactionRepository.deleteAll(); } } @Test public void getTransactionSuccessTest() { try { TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.SELL); transactionDTO.setStockValue("@501"); transactionDTO.setCommission(100.5); transactionDTO.setTax(5.5); transactionDTO.setClient("TESLA Technology"); transactionService.saveTransaction(transactionDTO); TransactionDTO transactionDTO1 = transactionService. findByTransactionCodeAndCompanySymbol("TSLA","NYSE1100011"); Assert.assertEquals("NYSE1100011", transactionDTO1.getTransactionCode()); Assert.assertEquals("TSLA", transactionDTO1.getCompanySymbol()); Assert.assertEquals("NY", transactionDTO1.getMarketCode()); } finally { transactionRepository.deleteAll(); } } @Test public void getTransactionPaginationSuccessTest() { try { // Create a transaction SELL TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.SELL); transactionDTO.setStockValue("@501"); transactionDTO.setCommission(100.5); transactionDTO.setTax(5.5); transactionDTO.setClient("TESLA Technology"); transactionService.saveTransaction(transactionDTO); // Create a transaction BUY transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.BUY); transactionDTO.setStockValue("@502"); transactionDTO.setCommission(100.5); transactionDTO.setTax(5.4); transactionDTO.setClient("TESLA Technology"); transactionService.saveTransaction(transactionDTO); Pagination<TransactionDTO> transactionPagination = transactionService.getAllTransactionsByPaging(0,10); Assert.assertNotNull(transactionPagination.getContent()); Assert.assertEquals("Should be page #0", 0L, transactionPagination.getPage()); Assert.assertEquals("Should be one page", 1L, transactionPagination.getTotalPages()); Assert.assertEquals("Found two transactions", 2, transactionPagination.getTotalResult()); } finally { transactionRepository.deleteAll(); } } }
5.2 Testing API.
Using MockMvc and MockRestServiceServer to mock the transaction APIs. Other ways, cucumber-junit, cucumber-spring,..are powerful integration testing.
src/test/java/com/mycompany/app/TransactionAPITest.java
package com.mycompany.app; import com.mycompany.app.transaction.Application; import com.mycompany.app.transaction.api.dto.TransactionDTO; import com.mycompany.app.transaction.domain.TransactionType; import com.mycompany.app.transaction.service.TransactionService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.test.web.servlet.MockMvc; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.WebApplicationContext; import java.io.IOException; import java.nio.charset.Charset; import java.util.Arrays; import java.util.stream.IntStream; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; /** * Created by Khiem on 10/29/2016. */ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = Application.class) @WebAppConfiguration public class TransactionAPITest { private MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); private MockMvc mockMvc; private MockRestServiceServer mockServer; private HttpMessageConverter mappingJackson2HttpMessageConverter; @Autowired private RestTemplate restTemplate; @Autowired private WebApplicationContext webApplicationContext; @Autowired private TransactionService transactionService; @Autowired void setConverters(HttpMessageConverter<?>[] converters) { this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter( hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get(); Assert.assertNotNull("the JSON message converter must not be null", this.mappingJackson2HttpMessageConverter); } @Before public void setup() throws Exception { this.mockMvc = webAppContextSetup(webApplicationContext).build(); mockServer = MockRestServiceServer.createServer(restTemplate); IntStream.range(1, 3).forEach(i -> { mockServer.expect(requestTo( "http://localhost:8081/api/tidgenerator?symbol=TSLA")) .andExpect(method(HttpMethod.GET)).andRespond( withSuccess().contentType(MediaType.APPLICATION_JSON).body( "{\"code\": \"NYSE110001" + i + "\"}")); }); initiateData(); } @Test public void createTransaction() throws Exception { // Client SELL @501 TSLA TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.SELL); transactionDTO.setStockValue("@501"); transactionDTO.setCommission(100.5); transactionDTO.setTax(5.5); transactionDTO.setClient("TESLA Technology"); mockMvc.perform(post("http://localhost:8082/api/transaction/add") .content(this.json(transactionDTO)) .contentType(contentType)) .andExpect(status().isOk()); } @Test public void readSingleTransaction() throws Exception { mockMvc.perform(get("http://localhost:8082/api/transaction/TSLA/NYSE1100012")) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.transactionCode", is("NYSE1100012"))) .andExpect(jsonPath("$.companySymbol", is("TSLA"))) .andExpect(jsonPath("$.marketCode", is("NY"))) .andExpect(jsonPath("$.transactionType", is("SELL"))) .andExpect(jsonPath("$.stockValue", is("@501"))) .andExpect(jsonPath("$.commission", is(100.5))) .andExpect(jsonPath("$.tax", is(5.5))) .andExpect(jsonPath("$.client", is("TESLA Technology"))); } @Test public void getTransactionlist() throws Exception { // Execute API to get list of transactions. String url = "http://localhost:8082/api/transaction/list?page=0&pageSize=10"; mockMvc.perform(get(url)) .andExpect(status().isOk()) .andExpect(content().contentType(contentType)) .andExpect(jsonPath("$.content", hasSize(2))) .andExpect(jsonPath("$.content.[0].transactionCode", is("NYSE1100012"))) .andExpect(jsonPath("$.content.[0].companySymbol", is("TSLA"))) .andExpect(jsonPath("$.content.[0].marketCode", is("NY"))) .andExpect(jsonPath("$.content.[0].transactionType", is("SELL"))) .andExpect(jsonPath("$.content.[0].commission", is(100.5))) .andExpect(jsonPath("$.content.[1].transactionCode", is("NYSE1100011"))) .andExpect(jsonPath("$.content.[1].companySymbol", is("TSLA"))) .andExpect(jsonPath("$.content.[1].marketCode", is("NY"))) .andExpect(jsonPath("$.content.[1].transactionType", is("BUY"))) .andExpect(jsonPath("$.content.[1].commission", is(100.4))); } protected String json(Object o) throws IOException { MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); this.mappingJackson2HttpMessageConverter.write( o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); return mockHttpOutputMessage.getBodyAsString(); } private void initiateData() throws Exception{ // Client BUY @501 TSLA TransactionDTO transactionDTO = new TransactionDTO(); transactionDTO.setCompanySymbol("TSLA"); transactionDTO.setMarketCode("NY"); transactionDTO.setTransactionType(TransactionType.BUY); transactionDTO.setStockValue("@501"); transactionDTO.setCommission(100.4); transactionDTO.setTax(5.3); transactionDTO.setClient("TESLA Technology"); transactionService.saveTransaction(transactionDTO); } }
References:
- Rodriguez, Alex. “RESTful Web services: The basics“. IBM developerWorks. IBM. Retrieved 30 Oct, 2016 (POST vs PUT – PUT is idempotent, so if you PUT an object twice, it has no effect. This is a nice property, so I would use PUT when possible.)
- Alex nordeen, RESTful Web Services Tutorial with Example. Retrieved 21 Sept 2018 at https://www.guru99.com/restful-web-services.html
- Toby Clemson, “Testing Strategies in a Microservice Architecture”, martinfowler. Retrieved Oct 30, 2016 at http://www.martinfowler.com/articles/microservice-testing/
- Jim W., Savas P. and Lan Robinson, REST in Practice, O’Reilly Media,2010.
- Sam Newman, “Rest”, in Buiding Microservice, O’Reilly Media, 2015.
- Addison Wesley – Enterprise Integration Patterns – Designing, Building And Deploying Messaging Solutions – With Notes
- JavaTM Persistence 2.1 Final Release for Evaluation, Oracle. Retrieved Oct 30, 2016 at
http://download.oracle.com/otndocs/jcp/persistence-2_1-fr-eval-spec/index.html - Building REST services with Spring, Pivotal Software, Inc. Retrieved Oct 30, 2016 at
https://spring.io/guides/tutorials/bookmarks/ - Transition to T+2 Settlement for Cash Equities, ASX Limited. Retrieved Oct 30, 2016 at
http://www.asx.com.au/services/t2.htm
Question: Why does entity classes (TransactionComposeKey) require at least a public or protect no-argument constructor (the class may have other constructors)?
Answer: Hibernate can produce a proxy (looks like a placeholder) which is an instance of a runtime -generated subclass of the class (TransactionComposeKey.class), carrying the identifier value of the entity instance it represents.