From 1c81b65544becb8989c7f8ff2885e1539f23360c Mon Sep 17 00:00:00 2001 From: Antoine Rey <antoine.rey@gmail.com> Date: Tue, 19 Mar 2019 18:26:35 +0100 Subject: [PATCH] Add JMeter load testing script --- README.md | 40 +- .../src/test/jmeter/petclinic_test_plan.jmx | 568 ++++++++++++++++++ .../customers/web/OwnerResource.java | 9 +- .../petclinic/customers/web/PetResource.java | 10 +- .../petclinic/visits/web/VisitResource.java | 6 +- 5 files changed, 606 insertions(+), 27 deletions(-) create mode 100644 spring-petclinic-api-gateway/src/test/jmeter/petclinic_test_plan.jmx diff --git a/README.md b/README.md index 8d131ff2..5f5fa653 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This microservices branch was initially derived from [AngularJS version](https:/ ## Starting services locally without Docker Every microservice is a Spring Boot application and can be started locally using IDE or `../mvnw spring-boot:run` command. Please note that supporting services (Config and Discovery Server) must be started before any other application (Customers, Vets, Visits and API). -Tracing server and Admin server startup is optional. +Startup of Tracing server, Admin server, Grafana and Prometheus is optional. If everything goes well, you can access the following services at given location: * Discovery Server - http://localhost:8761 * Config Server - http://localhost:8888 @@ -14,6 +14,8 @@ If everything goes well, you can access the following services at given location * Customers, Vets and Visits Services - random port, check Eureka Dashboard * Tracing Server (Zipkin) - http://localhost:9411/zipkin/ (we use [openzipkin](https://github.com/openzipkin/zipkin/tree/master/zipkin-server)) * Admin Server (Spring Boot Admin) - http://localhost:9090 +* Grafana Dashboards - http://localhost:3000 +* Prometheus - http://localhost:9091 * Hystrix Dashboard for Circuit Breaker pattern - http://localhost:7979 - On the home page is a form where you can enter the URL for an event stream to monitor, for example the `api-gateway` service running locally: `http://localhost:8080/actuator/hystrix.stream` or running into docker: `http://api-gateway:8080/actuator/hystrix.stream` @@ -87,29 +89,36 @@ the host and port of your MySQL JDBC connection string. ## Custom metrics monitoring -@todo Add default custom dashboards to grafana +Grafana and Prometheus are included in the `docker-compose.yml` configuration, and the public facing applications +have been instrumented with [MicroMeter](https://micrometer.io) to collect JVM and custom business metrics. -Grafana and Prometheus are included in the `docker-compose.yml` configuration, and the public facing applications have been instrumented with [MicroMeter](https://micrometer.io) to collect JVM and custom business metrics. +A JMeter load testing script is available to stress the application and generate metrics: [petclinic_test_plan.jmx](spring-petclinic-api-gateway/src/test/jmeter/petclinic_test_plan.jmx) + + ### Using Prometheus -* Prometheus can be accessed from your local machine at http://localhost:9091 +* Prometheus can be accessed from your local machine at [http://localhost:9091]() ### Using Grafana with Prometheus -* Login to Grafana at http://localhost:3000, the default user/pass is `admin:admin`, you will be prompted to change your password. -* Setup a prometheus datasource and point the URL to `http://prometheus-server:9090`, leave all the other options set to their default. -* Add the [Micrometer/SpringBoot dashboard](https://grafana.com/dashboards/4701) via the Import Dashboard menu item. The id for the dashboard is `4701` +* An anonymous access and a Prometheus datasource are setup. +* A `Spring Petclinic Metrics` Dashboard is available at the URL [http://localhost:3000/d/69JXeR0iw/spring-petclinic-metrics](). +You will find the JSON configuration file here: [docker/grafana/dashboards/grafana-petclinic-dashboard.json](). +* You may create your own dashboard or import the [Micrometer/SpringBoot dashboard](https://grafana.com/dashboards/4701) via the Import Dashboard menu item. +The id for this dashboard is `4701`. + +### Custom metrics -### Custom metrics implementation +Spring Boot registers a lot number of core metrics: JVM, CPU, Tomcat, Logback... +The Spring Boot auto-configuration enables the instrumentation of requests handled by Spring MVC. +All those three REST controllers `OwnerResource`, `PetResource` and `VisitResource` have been instrumented by the `@Timed` Micrometer annotation at class level. * `customers-service` application has the following custom metrics enabled: - * counter: `create.owner` - * counter: `update.owner` - * counter: `create.pet` - * counter: `update.pet` + * @Timed: `petclinic.owner` + * @Timed: `petclinic.pet` * `visits-service` application has the following custom metrics enabled: - * counter: `create.visit` + * @Timed: `petclinic.visit` ## Looking for something in particular? @@ -119,8 +128,8 @@ Grafana and Prometheus are included in the `docker-compose.yml` configuration, a | Service Discovery | [Eureka server](spring-petclinic-discovery-server) and [Service discovery client](spring-petclinic-vets-service/src/main/java/org/springframework/samples/petclinic/vets/VetsServiceApplication.java) | | API Gateway | [Zuul reverse proxy](spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java) and [Routing configuration](https://github.com/spring-petclinic/spring-petclinic-microservices-config/blob/master/api-gateway.yml) | | Docker Compose | [Spring Boot with Docker guide](https://spring.io/guides/gs/spring-boot-docker/) and [docker-compose file](docker-compose.yml) | -| Circuit Breaker | TBD | -| Grafana / Prometheus Monitoring | [Micrometer implementation](https://micrometer.io/) | +| Circuit Breaker | [Hystrix fallback method](spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java) | +| Grafana / Prometheus Monitoring | [Micrometer implementation](https://micrometer.io/), [Spring Boot Actuator Production Ready Metrics] | Front-end module | Files | |-------------------|-------| @@ -149,3 +158,4 @@ For pull requests, editor preferences are available in the [editor config](.edit [Configuration repository]: https://github.com/spring-petclinic/spring-petclinic-microservices-config +[Spring Boot Actuator Production Ready Metrics]: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-metrics.html diff --git a/spring-petclinic-api-gateway/src/test/jmeter/petclinic_test_plan.jmx b/spring-petclinic-api-gateway/src/test/jmeter/petclinic_test_plan.jmx new file mode 100644 index 00000000..9e88f1a8 --- /dev/null +++ b/spring-petclinic-api-gateway/src/test/jmeter/petclinic_test_plan.jmx @@ -0,0 +1,568 @@ +<?xml version="1.0" encoding="UTF-8"?> +<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.0 r1840935"> + <hashTree> + <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Spring Petclinic Microservices" enabled="true"> + <stringProp name="TestPlan.comments"></stringProp> + <boolProp name="TestPlan.functional_mode">false</boolProp> + <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp> + <boolProp name="TestPlan.serialize_threadgroups">false</boolProp> + <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"> + <collectionProp name="Arguments.arguments"> + <elementProp name="PETCLINC_HOST" elementType="Argument"> + <stringProp name="Argument.name">PETCLINC_HOST</stringProp> + <stringProp name="Argument.value">localhost</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + <elementProp name="PETCLINIC_PORT" elementType="Argument"> + <stringProp name="Argument.name">PETCLINIC_PORT</stringProp> + <stringProp name="Argument.value">8080</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="TestPlan.user_define_classpath"></stringProp> + </TestPlan> + <hashTree> + <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true"> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr�-d�finies" enabled="true"> + <collectionProp name="Arguments.arguments"/> + </elementProp> + <stringProp name="HTTPSampler.domain">${PETCLINC_HOST}</stringProp> + <stringProp name="HTTPSampler.port">${PETCLINIC_PORT}</stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path"></stringProp> + <stringProp name="HTTPSampler.concurrentPool">6</stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </ConfigTestElement> + <hashTree/> + <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true"> + <collectionProp name="HeaderManager.headers"> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Accept</stringProp> + <stringProp name="Header.value">application/json, text/plain, */*</stringProp> + </elementProp> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Content-Type</stringProp> + <stringProp name="Header.value">application/json;charset=UTF-8</stringProp> + </elementProp> + <elementProp name="" elementType="Header"> + <stringProp name="Header.name">Accept-Encoding</stringProp> + <stringProp name="Header.value">gzip, deflate, br</stringProp> + </elementProp> + </collectionProp> + </HeaderManager> + <hashTree/> + <kg.apc.jmeter.threads.UltimateThreadGroup guiclass="kg.apc.jmeter.threads.UltimateThreadGroupGui" testclass="kg.apc.jmeter.threads.UltimateThreadGroup" testname="jp@gc - Ultimate Thread Group" enabled="true"> + <collectionProp name="ultimatethreadgroupdata"> + <collectionProp name="-111815413"> + <stringProp name="49">1</stringProp> + <stringProp name="0">0</stringProp> + <stringProp name="48">0</stringProp> + <stringProp name="50547">300</stringProp> + <stringProp name="1629">30</stringProp> + </collectionProp> + </collectionProp> + <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true"> + <boolProp name="LoopController.continue_forever">false</boolProp> + <intProp name="LoopController.loops">-1</intProp> + </elementProp> + <stringProp name="ThreadGroup.on_sample_error">continue</stringProp> + </kg.apc.jmeter.threads.UltimateThreadGroup> + <hashTree> + <RandomVariableConfig guiclass="TestBeanGUI" testclass="RandomVariableConfig" testname="Pet Type" enabled="true"> + <stringProp name="variableName">PET_TYPE</stringProp> + <stringProp name="outputFormat"></stringProp> + <stringProp name="minimumValue">1</stringProp> + <stringProp name="maximumValue">6</stringProp> + <stringProp name="randomSeed"></stringProp> + <boolProp name="perThread">false</boolProp> + </RandomVariableConfig> + <hashTree/> + <GaussianRandomTimer guiclass="GaussianRandomTimerGui" testclass="GaussianRandomTimer" testname="Gaussian Random Timer" enabled="true"> + <stringProp name="ConstantTimer.delay">300</stringProp> + <stringProp name="RandomTimer.range">100.0</stringProp> + </GaussianRandomTimer> + <hashTree/> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="All Owners" enabled="true"> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr�-d�finies" enabled="true"> + <collectionProp name="Arguments.arguments"/> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners</stringProp> + <stringProp name="HTTPSampler.method">GET</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49586">200</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Owner" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "firstName":"Firstname",
 + "lastName":"Lastname",
 + "address":"Adress",
 + "city":"City",
 + "telephone":"0000000000"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49587">201</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Owner ID Extractor" enabled="true"> + <stringProp name="JSONPostProcessor.referenceNames">OWNER_ID</stringProp> + <stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp> + <stringProp name="JSONPostProcessor.match_numbers"></stringProp> + </JSONPostProcessor> + <hashTree/> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Owner Details" enabled="true"> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr�-d�finies" enabled="true"> + <collectionProp name="Arguments.arguments"/> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/gateway/owners/${OWNER_ID}</stringProp> + <stringProp name="HTTPSampler.method">GET</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49586">200</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Update Owner" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "id":"${OWNER_ID}",
 + "firstName":"Firstname",
 + "lastName":"Lastname${OWNER_ID}",
 + "address":"Adress${OWNER_ID}",
 + "city":"City${OWNER_ID}",
 + "telephone":"1111111111"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}</stringProp> + <stringProp name="HTTPSampler.method">PUT</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 204" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49590">204</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Pet" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "name":"Pet",
 + "birthDate":"2018-12-31T23:00:00.000Z",
 + "typeId":"${PET_TYPE}"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49587">201</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + <JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="Pet ID Extractor" enabled="true"> + <stringProp name="JSONPostProcessor.referenceNames">PET_ID</stringProp> + <stringProp name="JSONPostProcessor.jsonPathExprs">$.id</stringProp> + <stringProp name="JSONPostProcessor.match_numbers"></stringProp> + </JSONPostProcessor> + <hashTree/> + </hashTree> + <RandomController guiclass="RandomControlGui" testclass="RandomController" testname="Add Random second Pet" enabled="true"> + <intProp name="InterleaveControl.style">1</intProp> + </RandomController> + <hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Pet" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "name":"Pet",
 + "birthDate":"2018-12-31T23:00:00.000Z",
 + "typeId":"${PET_TYPE}"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49587">201</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Update Pet" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "id": ${PET_ID},
 + "name":"Pet${OWNER_ID}",
 + "birthDate":"2018-12-31T23:00:00.000Z",
 + "typeId":"${PET_TYPE}"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/customer/owners/${OWNER_ID}/pets/${PET_ID}</stringProp> + <stringProp name="HTTPSampler.method">PUT</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 204" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49590">204</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Visit" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "date":"2019-03-15",
 + "description":"Visit"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/visit/owners/${OWNER_ID}/pets/${PET_ID}/visits</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49587">201</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <RandomController guiclass="RandomControlGui" testclass="RandomController" testname="Add Random second visit" enabled="true"> + <intProp name="InterleaveControl.style">1</intProp> + </RandomController> + <hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Add Visit" enabled="true"> + <boolProp name="HTTPSampler.postBodyRaw">true</boolProp> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments"> + <collectionProp name="Arguments.arguments"> + <elementProp name="" elementType="HTTPArgument"> + <boolProp name="HTTPArgument.always_encode">false</boolProp> + <stringProp name="Argument.value">{ 
 + "date":"2019-03-15",
 + "description":"Visit"
 +}</stringProp> + <stringProp name="Argument.metadata">=</stringProp> + </elementProp> + </collectionProp> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/visit/owners/${OWNER_ID}/pets/${PET_ID}/visits</stringProp> + <stringProp name="HTTPSampler.method">POST</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 201" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49587">201</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + </hashTree> + <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="All Vets" enabled="true"> + <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="Variables pr�-d�finies" enabled="true"> + <collectionProp name="Arguments.arguments"/> + </elementProp> + <stringProp name="HTTPSampler.domain"></stringProp> + <stringProp name="HTTPSampler.port"></stringProp> + <stringProp name="HTTPSampler.protocol"></stringProp> + <stringProp name="HTTPSampler.contentEncoding"></stringProp> + <stringProp name="HTTPSampler.path">/api/vet/vets</stringProp> + <stringProp name="HTTPSampler.method">GET</stringProp> + <boolProp name="HTTPSampler.follow_redirects">true</boolProp> + <boolProp name="HTTPSampler.auto_redirects">false</boolProp> + <boolProp name="HTTPSampler.use_keepalive">true</boolProp> + <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp> + <stringProp name="HTTPSampler.embedded_url_re"></stringProp> + <stringProp name="HTTPSampler.connect_timeout"></stringProp> + <stringProp name="HTTPSampler.response_timeout"></stringProp> + </HTTPSamplerProxy> + <hashTree> + <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Assertion HTTP 200" enabled="true"> + <collectionProp name="Asserion.test_strings"> + <stringProp name="49586">200</stringProp> + </collectionProp> + <stringProp name="Assertion.custom_message"></stringProp> + <stringProp name="Assertion.test_field">Assertion.response_code</stringProp> + <boolProp name="Assertion.assume_success">false</boolProp> + <intProp name="Assertion.test_type">8</intProp> + </ResponseAssertion> + <hashTree/> + </hashTree> + <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true"> + <boolProp name="ResultCollector.error_logging">false</boolProp> + <objProp> + <name>saveConfig</name> + <value class="SampleSaveConfiguration"> + <time>true</time> + <latency>true</latency> + <timestamp>true</timestamp> + <success>true</success> + <label>true</label> + <code>true</code> + <message>true</message> + <threadName>true</threadName> + <dataType>true</dataType> + <encoding>false</encoding> + <assertions>true</assertions> + <subresults>true</subresults> + <responseData>false</responseData> + <samplerData>false</samplerData> + <xml>false</xml> + <fieldNames>true</fieldNames> + <responseHeaders>false</responseHeaders> + <requestHeaders>false</requestHeaders> + <responseDataOnError>false</responseDataOnError> + <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> + <assertionsResultsToSave>0</assertionsResultsToSave> + <bytes>true</bytes> + <sentBytes>true</sentBytes> + <url>true</url> + <threadCounts>true</threadCounts> + <idleTime>true</idleTime> + <connectTime>true</connectTime> + </value> + </objProp> + <stringProp name="filename"></stringProp> + </ResultCollector> + <hashTree/> + <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true"> + <boolProp name="ResultCollector.error_logging">false</boolProp> + <objProp> + <name>saveConfig</name> + <value class="SampleSaveConfiguration"> + <time>true</time> + <latency>true</latency> + <timestamp>true</timestamp> + <success>true</success> + <label>true</label> + <code>true</code> + <message>true</message> + <threadName>true</threadName> + <dataType>true</dataType> + <encoding>false</encoding> + <assertions>true</assertions> + <subresults>true</subresults> + <responseData>false</responseData> + <samplerData>false</samplerData> + <xml>false</xml> + <fieldNames>true</fieldNames> + <responseHeaders>false</responseHeaders> + <requestHeaders>false</requestHeaders> + <responseDataOnError>false</responseDataOnError> + <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage> + <assertionsResultsToSave>0</assertionsResultsToSave> + <bytes>true</bytes> + <sentBytes>true</sentBytes> + <url>true</url> + <threadCounts>true</threadCounts> + <idleTime>true</idleTime> + <connectTime>true</connectTime> + </value> + </objProp> + <stringProp name="filename"></stringProp> + </ResultCollector> + <hashTree/> + </hashTree> + </hashTree> + </hashTree> +</jmeterTestPlan> diff --git a/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java b/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java index 17910434..c9ea4b92 100644 --- a/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java +++ b/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/OwnerResource.java @@ -48,8 +48,8 @@ class OwnerResource { */ @PostMapping @ResponseStatus(HttpStatus.CREATED) - public void createOwner(@Valid @RequestBody Owner owner) { - ownerRepository.save(owner); + public Owner createOwner(@Valid @RequestBody Owner owner) { + return ownerRepository.save(owner); } /** @@ -72,7 +72,8 @@ class OwnerResource { * Update Owner */ @PutMapping(value = "/{ownerId}") - public Owner updateOwner(@PathVariable("ownerId") int ownerId, @Valid @RequestBody Owner ownerRequest) { + @ResponseStatus(HttpStatus.NO_CONTENT) + public void updateOwner(@PathVariable("ownerId") int ownerId, @Valid @RequestBody Owner ownerRequest) { final Optional<Owner> owner = ownerRepository.findById(ownerId); final Owner ownerModel = owner.orElseThrow(() -> new ResourceNotFoundException("Owner "+ownerId+" not found")); @@ -83,6 +84,6 @@ class OwnerResource { ownerModel.setAddress(ownerRequest.getAddress()); ownerModel.setTelephone(ownerRequest.getTelephone()); log.info("Saving owner {}", ownerModel); - return ownerRepository.save(ownerModel); + ownerRepository.save(ownerModel); } } diff --git a/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java b/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java index 3bb7f7bf..3b8bdd88 100644 --- a/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java +++ b/spring-petclinic-customers-service/src/main/java/org/springframework/samples/petclinic/customers/web/PetResource.java @@ -47,8 +47,8 @@ class PetResource { } @PostMapping("/owners/{ownerId}/pets") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void processCreationForm( + @ResponseStatus(HttpStatus.CREATED) + public Pet processCreationForm( @RequestBody PetRequest petRequest, @PathVariable("ownerId") int ownerId) { @@ -57,7 +57,7 @@ class PetResource { Owner owner = optionalOwner.orElseThrow(() -> new ResourceNotFoundException("Owner "+ownerId+" not found")); owner.addPet(pet); - save(pet, petRequest); + return save(pet, petRequest); } @PutMapping("/owners/*/pets/{petId}") @@ -68,7 +68,7 @@ class PetResource { save(pet, petRequest); } - private void save(final Pet pet, final PetRequest petRequest) { + private Pet save(final Pet pet, final PetRequest petRequest) { pet.setName(petRequest.getName()); pet.setBirthDate(petRequest.getBirthDate()); @@ -77,7 +77,7 @@ class PetResource { .ifPresent(pet::setType); log.info("Saving pet {}", pet); - petRepository.save(pet); + return petRepository.save(pet); } @GetMapping("owners/*/pets/{petId}") diff --git a/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java b/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java index 8f6bd5ca..3bcf7f76 100644 --- a/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java +++ b/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/web/VisitResource.java @@ -49,14 +49,14 @@ class VisitResource { private final VisitRepository visitRepository; @PostMapping("owners/*/pets/{petId}/visits") - @ResponseStatus(HttpStatus.NO_CONTENT) - void create( + @ResponseStatus(HttpStatus.CREATED) + Visit create( @Valid @RequestBody Visit visit, @PathVariable("petId") int petId) { visit.setPetId(petId); log.info("Saving visit {}", visit); - visitRepository.save(visit); + return visitRepository.save(visit); } @GetMapping("owners/*/pets/{petId}/visits") -- GitLab