From 708e85ce38e7bfaaea423e5ea3995fb7abf4aacb Mon Sep 17 00:00:00 2001
From: Sampada <46674082+sampada07@users.noreply.github.com>
Date: Sat, 16 May 2020 02:07:36 +0530
Subject: [PATCH] BAEL-3493: Spring REST Docs vs OpenAPI (#9295)
---
.../spring-boot-springdoc/pom.xml | 50 +++++-
.../baeldung/restdocopenapi/Application.java | 28 ++++
.../java/com/baeldung/restdocopenapi/Foo.java | 93 +++++++++++
.../restdocopenapi/FooController.java | 83 ++++++++++
.../restdocopenapi/FooRepository.java | 9 ++
.../springdoc/FooBarController.java | 121 ++++++++++++++
.../src/main/resources/application.properties | 5 +-
.../src/main/resources/asciidocs/fooapi.adoc | 153 ++++++++++++++++++
.../src/main/resources/data.sql | 4 +
.../src/main/resources/schema.sql | 8 +
.../restdoc/SpringRestDocsUnitTest.java | 120 ++++++++++++++
11 files changed, 672 insertions(+), 2 deletions(-)
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Application.java
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Foo.java
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooController.java
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooRepository.java
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/springdoc/FooBarController.java
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/resources/asciidocs/fooapi.adoc
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/resources/data.sql
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/main/resources/schema.sql
create mode 100644 spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/restdocopenapi/restdoc/SpringRestDocsUnitTest.java
diff --git a/spring-boot-modules/spring-boot-springdoc/pom.xml b/spring-boot-modules/spring-boot-springdoc/pom.xml
index 375cf06c2c..1c1f27b5a5 100644
--- a/spring-boot-modules/spring-boot-springdoc/pom.xml
+++ b/spring-boot-modules/spring-boot-springdoc/pom.xml
@@ -37,7 +37,7 @@
test
-
+
org.hibernate
hibernate-core
${hibernate.version}
@@ -53,7 +53,19 @@
org.springdoc
springdoc-openapi-data-rest
${springdoc.version}
+
+
+
+
+ org.springframework.restdocs
+ spring-restdocs-mockmvc
+ test
+
+ org.springframework.restdocs
+ spring-restdocs-restassured
+ test
+
@@ -62,13 +74,49 @@
org.springframework.boot
spring-boot-maven-plugin
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+ ${asciidoctor-plugin.version}
+
+
+ generate-docs
+ package
+
+ process-asciidoc
+
+
+ html
+ book
+
+ ${snippetsDirectory}
+
+ src/main/resources/asciidocs
+ target/generated-docs
+
+
+
+
+
+
+ true
+ src/main/resources
+
+ application.properties
+ data.sql
+ schema.sql
+
+
+
1.8
5.2.10.Final
1.2.32
+ 1.5.6
+ ${project.build.directory}/generated-snippets
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Application.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Application.java
new file mode 100644
index 0000000000..93a28a2b49
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Application.java
@@ -0,0 +1,28 @@
+package com.baeldung.restdocopenapi;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+
+@SpringBootApplication()
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+ @Bean
+ public OpenAPI customOpenAPI(@Value("${springdoc.version}") String appVersion) {
+ return new OpenAPI().info(new Info().title("Foobar API")
+ .version(appVersion)
+ .description("This is a sample Foobar server created using springdocs - a library for OpenAPI 3 with spring boot.")
+ .termsOfService("http://swagger.io/terms/")
+ .license(new License().name("Apache 2.0")
+ .url("http://springdoc.org")));
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Foo.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Foo.java
new file mode 100644
index 0000000000..99d63581be
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/Foo.java
@@ -0,0 +1,93 @@
+package com.baeldung.restdocopenapi;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+
+@Entity
+public class Foo {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private String title;
+
+ @Column()
+ private String body;
+
+
+ protected Foo() {
+ }
+
+ public Foo(long id, String title, String body) {
+ this.id = id;
+ this.title = title;
+ this.body = body;
+ }
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ result = prime * result + ((title == null) ? 0 : title.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Foo other = (Foo) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ if (title == null) {
+ if (other.title != null)
+ return false;
+ } else if (!title.equals(other.title))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "Foo [id=" + id + ", title=" + title + "]";
+ }
+}
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooController.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooController.java
new file mode 100644
index 0000000000..55c2cccb3c
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooController.java
@@ -0,0 +1,83 @@
+package com.baeldung.restdocopenapi;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.validation.Valid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/foo")
+public class FooController {
+
+ @Autowired
+ FooRepository repository;
+
+ @GetMapping
+ public ResponseEntity> getAllFoos() {
+ List fooList = (List) repository.findAll();
+ if (fooList.isEmpty()) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ return new ResponseEntity<>(fooList, HttpStatus.OK);
+ }
+
+ @GetMapping(value = "{id}")
+ public ResponseEntity getFooById(@PathVariable("id") Long id) {
+
+ Optional foo = repository.findById(id);
+ return foo.isPresent() ? new ResponseEntity<>(foo.get(), HttpStatus.OK) : new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+
+ @PostMapping
+ public ResponseEntity addFoo(@RequestBody @Valid Foo foo) {
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.setLocation(linkTo(FooController.class).slash(foo.getId())
+ .toUri());
+ Foo savedFoo;
+ try {
+ savedFoo = repository.save(foo);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+
+ return new ResponseEntity<>(savedFoo, httpHeaders, HttpStatus.CREATED);
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteFoo(@PathVariable("id") long id) {
+ try {
+ repository.deleteById(id);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+
+ @PutMapping("/{id}")
+ public ResponseEntity updateFoo(@PathVariable("id") long id, @RequestBody Foo foo) {
+ boolean isFooPresent = repository.existsById(Long.valueOf(id));
+
+ if (!isFooPresent) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+
+ Foo updatedFoo = repository.save(foo);
+
+ return new ResponseEntity<>(updatedFoo, HttpStatus.OK);
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooRepository.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooRepository.java
new file mode 100644
index 0000000000..105b57b2ef
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/FooRepository.java
@@ -0,0 +1,9 @@
+package com.baeldung.restdocopenapi;
+
+import org.springframework.data.repository.PagingAndSortingRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface FooRepository extends PagingAndSortingRepository{
+
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/springdoc/FooBarController.java b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/springdoc/FooBarController.java
new file mode 100644
index 0000000000..8af414c8fd
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/java/com/baeldung/restdocopenapi/springdoc/FooBarController.java
@@ -0,0 +1,121 @@
+package com.baeldung.restdocopenapi.springdoc;
+
+import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.validation.Valid;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.baeldung.restdocopenapi.Foo;
+import com.baeldung.restdocopenapi.FooRepository;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+@RestController
+@RequestMapping("/foobar")
+@Tag(name = "foobar", description = "the foobar API with documentation annotations")
+public class FooBarController {
+
+ @Autowired
+ FooRepository repository;
+
+ @Operation(summary = "Get all foos")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "found foos", content = {
+ @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = Foo.class)))}),
+ @ApiResponse(responseCode = "404", description = "No Foos found", content = @Content) })
+ @GetMapping
+ public ResponseEntity> getAllFoos() {
+ List fooList = (List) repository.findAll();
+ if (fooList.isEmpty()) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+ return new ResponseEntity<>(fooList, HttpStatus.OK);
+ }
+
+ @Operation(summary = "Get a foo by foo id")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "found the foo", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation = Foo.class))}),
+ @ApiResponse(responseCode = "400", description = "Invalid id supplied", content = @Content),
+ @ApiResponse(responseCode = "404", description = "Foo not found", content = @Content) })
+ @GetMapping(value = "{id}")
+ public ResponseEntity getFooById(@Parameter(description = "id of foo to be searched") @PathVariable("id") String id) {
+
+ Optional foo = repository.findById(Long.valueOf(id));
+ return foo.isPresent() ? new ResponseEntity<>(foo.get(), HttpStatus.OK) : new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+
+ @Operation(summary = "Create a foo")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "201", description = "foo created", content = { @
+ Content(mediaType = "application/json", schema = @Schema(implementation = Foo.class))}),
+ @ApiResponse(responseCode = "404", description = "Bad request", content = @Content) })
+ @PostMapping
+ public ResponseEntity addFoo(@Parameter(description = "foo object to be created") @RequestBody @Valid Foo foo) {
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.setLocation(linkTo(FooBarController.class).slash(foo.getId()).toUri());
+ Foo savedFoo;
+ try {
+ savedFoo = repository.save(foo);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+
+ return new ResponseEntity<>(savedFoo, httpHeaders, HttpStatus.CREATED);
+ }
+
+ @Operation(summary = "Delete a foo")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "204", description = "foo deleted"),
+ @ApiResponse(responseCode = "404", description = "Bad request", content = @Content) })
+ @DeleteMapping("/{id}")
+ public ResponseEntity deleteFoo(@Parameter(description = "id of foo to be deleted") @PathVariable("id") long id) {
+ try {
+ repository.deleteById(id);
+ } catch (Exception e) {
+ return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
+ }
+ return new ResponseEntity<>(HttpStatus.NO_CONTENT);
+ }
+
+ @Operation(summary = "Update a foo")
+ @ApiResponses(value = {
+ @ApiResponse(responseCode = "200", description = "foo updated successfully", content = {
+ @Content(mediaType = "application/json", schema = @Schema(implementation = Foo.class))}),
+ @ApiResponse(responseCode = "404", description = "No Foo exists with given id", content = @Content) })
+ @PutMapping("/{id}")
+ public ResponseEntity updateFoo(@Parameter(description = "id of foo to be updated") @PathVariable("id") long id, @RequestBody Foo foo) {
+
+ boolean isFooPresent = repository.existsById(Long.valueOf(id));
+
+ if (!isFooPresent) {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+
+ Foo updatedFoo = repository.save(foo);
+
+ return new ResponseEntity<>(updatedFoo, HttpStatus.OK);
+ }
+}
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties b/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties
index 45378e610b..4191d8ba25 100644
--- a/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/application.properties
@@ -5,4 +5,7 @@ springdoc.swagger-ui.path=/swagger-ui-custom.html
springdoc.api-docs.path=/api-docs
# H2 Related Configurations
-spring.datasource.url=jdbc:h2:mem:springdoc
\ No newline at end of file
+spring.datasource.url=jdbc:h2:mem:springdoc
+
+springdoc.version=@springdoc.version@
+spring.jpa.hibernate.ddl-auto=none
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/asciidocs/fooapi.adoc b/spring-boot-modules/spring-boot-springdoc/src/main/resources/asciidocs/fooapi.adoc
new file mode 100644
index 0000000000..90791cbbcf
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/asciidocs/fooapi.adoc
@@ -0,0 +1,153 @@
+= RESTful Notes API Guide
+Baeldung;
+:doctype: book
+:icons: font
+:source-highlighter: highlightjs
+:toc: left
+:toclevels: 4
+:sectlinks:
+
+[[overview]]
+= Overview
+
+[[overview-http-verbs]]
+== HTTP verbs
+
+RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
+use of HTTP verbs.
+
+|===
+| Verb | Usage
+
+| `GET`
+| Used to retrieve a resource
+
+| `POST`
+| Used to create a new resource
+
+| `PUT`
+| Used to update an existing resource
+
+| `DELETE`
+| Used to delete an existing resource
+|===
+
+RESTful notes tries to adhere as closely as possible to standard HTTP and REST conventions in its
+use of HTTP status codes.
+
+|===
+| Status code | Usage
+
+| `200 OK`
+| The request completed successfully
+
+| `201 Created`
+| A new resource has been created successfully. The resource's URI is available from the response's
+`Location` header
+
+| `204 No Content`
+| An update to an existing resource has been applied successfully
+
+| `400 Bad Request`
+| The request was malformed. The response body will include an error providing further information
+
+| `404 Not Found`
+| The requested resource did not exist
+|===
+
+[[overview-hypermedia]]
+== Hypermedia
+
+RESTful Notes uses hypermedia and resources include links to other resources in their
+responses. Responses are in http://stateless.co/hal_specification.html[Hypertext Application
+from resource to resource.
+Language (HAL)] format. Links can be found beneath the `_links` key. Users of the API should
+not create URIs themselves, instead they should use the above-described links to navigate
+
+[[resources]]
+= Resources
+
+[[resources-FOO]]
+== FOO REST Service
+
+The FOO provides the entry point into the service.
+
+[[resources-foo-get]]
+=== Accessing the foo GET
+
+A `GET` request is used to access the foo read.
+
+==== Request structure
+
+include::{snippets}/getAFoo/http-request.adoc[]
+
+==== Path Parameters
+include::{snippets}/getAFoo/path-parameters.adoc[]
+
+==== Example response
+
+include::{snippets}/getAFoo/http-response.adoc[]
+
+==== CURL request
+
+include::{snippets}/getAFoo/curl-request.adoc[]
+
+[[resources-foo-post]]
+=== Accessing the foo POST
+
+A `POST` request is used to access the foo create.
+
+==== Request structure
+
+include::{snippets}/createFoo/http-request.adoc[]
+
+==== Example response
+
+include::{snippets}/createFoo/http-response.adoc[]
+
+==== CURL request
+
+include::{snippets}/createFoo/curl-request.adoc[]
+
+[[resources-foo-delete]]
+=== Accessing the foo DELETE
+
+A `DELETE` request is used to access the foo delete.
+
+==== Request structure
+
+include::{snippets}/deleteFoo/http-request.adoc[]
+
+==== Path Parameters
+include::{snippets}/deleteFoo/path-parameters.adoc[]
+
+==== Example response
+
+include::{snippets}/deleteFoo/http-response.adoc[]
+
+==== CURL request
+
+include::{snippets}/deleteFoo/curl-request.adoc[]
+
+[[resources-foo-put]]
+=== Accessing the foo PUT
+
+A `PUT` request is used to access the foo update.
+
+==== Request structure
+
+include::{snippets}/updateFoo/http-request.adoc[]
+
+==== Path Parameters
+include::{snippets}/updateFoo/path-parameters.adoc[]
+
+==== Example response
+
+include::{snippets}/updateFoo/http-response.adoc[]
+
+==== CURL request
+
+include::{snippets}/updateFoo/curl-request.adoc[]
+
+
+
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/data.sql b/spring-boot-modules/spring-boot-springdoc/src/main/resources/data.sql
new file mode 100644
index 0000000000..f80e53b717
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/data.sql
@@ -0,0 +1,4 @@
+INSERT INTO Foo(id, title, body) VALUES (1, 'Foo 1', 'Foo body 1');
+INSERT INTO Foo(id, title, body) VALUES (2, 'Foo 2', 'Foo body 2');
+INSERT INTO Foo(id, title, body) VALUES (3, 'Foo 3', 'Foo body 3');
+
diff --git a/spring-boot-modules/spring-boot-springdoc/src/main/resources/schema.sql b/spring-boot-modules/spring-boot-springdoc/src/main/resources/schema.sql
new file mode 100644
index 0000000000..e5d33da019
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/main/resources/schema.sql
@@ -0,0 +1,8 @@
+DROP TABLE IF EXISTS foo;
+
+CREATE TABLE foo (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ title VARCHAR(250) NOT NULL,
+ body VARCHAR(250),
+ PRIMARY KEY (id)
+);
\ No newline at end of file
diff --git a/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/restdocopenapi/restdoc/SpringRestDocsUnitTest.java b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/restdocopenapi/restdoc/SpringRestDocsUnitTest.java
new file mode 100644
index 0000000000..4d37abf78a
--- /dev/null
+++ b/spring-boot-modules/spring-boot-springdoc/src/test/java/com/baeldung/restdocopenapi/restdoc/SpringRestDocsUnitTest.java
@@ -0,0 +1,120 @@
+package com.baeldung.restdocopenapi.restdoc;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
+import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.put;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
+import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
+import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
+import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
+import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
+import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
+import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.util.StringUtils.collectionToDelimitedString;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.restdocs.RestDocumentationContextProvider;
+import org.springframework.restdocs.RestDocumentationExtension;
+import org.springframework.restdocs.constraints.ConstraintDescriptions;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.baeldung.restdocopenapi.Application;
+import com.baeldung.restdocopenapi.Foo;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@ExtendWith({ RestDocumentationExtension.class, SpringExtension.class })
+@SpringBootTest(classes = Application.class)
+public class SpringRestDocsUnitTest {
+
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ public void setup(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
+ .apply(documentationConfiguration(restDocumentation))
+ .build();
+ }
+
+ @Test
+ public void whenGetFoo_thenSuccessful() throws Exception {
+ this.mockMvc.perform(get("/foo"))
+ .andDo(print())
+ .andExpect(status().isOk())
+ .andExpect(content().string(containsString("Foo 1")))
+ .andDo(document("getAllFoos"));
+ }
+
+ @Test
+ public void whenGetFooById_thenSuccessful() throws Exception {
+ ConstraintDescriptions desc = new ConstraintDescriptions(Foo.class);
+
+ this.mockMvc.perform(get("/foo/{id}", 1))
+ .andExpect(status().isOk())
+ .andDo(document("getAFoo", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
+ pathParameters(parameterWithName("id").description("id of foo to be searched")),
+ responseFields(fieldWithPath("id").description("The id of the foo" + collectionToDelimitedString(desc.descriptionsForProperty("id"), ". ")),
+ fieldWithPath("title").description("The title of the foo"), fieldWithPath("body").description("The body of the foo"))));
+ }
+
+ @Test
+ public void whenPostFoo_thenSuccessful() throws Exception {
+ Map foo = new HashMap<>();
+ foo.put("id", 4L);
+ foo.put("title", "New Foo");
+ foo.put("body", "Body of New Foo");
+
+ this.mockMvc.perform(post("/foo").contentType(MediaTypes.HAL_JSON)
+ .content(this.objectMapper.writeValueAsString(foo)))
+ .andExpect(status().isCreated())
+ .andDo(document("createFoo", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()), requestFields(fieldWithPath("id").description("The id of the foo"), fieldWithPath("title").description("The title of the foo"),
+ fieldWithPath("body").description("The body of the foo"))));
+ }
+
+ @Test
+ public void whenDeleteFoo_thenSuccessful() throws Exception {
+ this.mockMvc.perform(delete("/foo/{id}", 2))
+ .andExpect(status().isNoContent())
+ .andDo(document("deleteFoo", pathParameters(parameterWithName("id").description("The id of the foo to delete"))));
+ }
+
+ @Test
+ public void whenUpdateFoo_thenSuccessful() throws Exception {
+
+ ConstraintDescriptions desc = new ConstraintDescriptions(Foo.class);
+
+ Map foo = new HashMap<>();
+ foo.put("title", "Updated Foo");
+ foo.put("body", "Body of Updated Foo");
+
+ this.mockMvc.perform(put("/foo/{id}", 3).contentType(MediaTypes.HAL_JSON)
+ .content(this.objectMapper.writeValueAsString(foo)))
+ .andExpect(status().isOk())
+ .andDo(document("updateFoo", pathParameters(parameterWithName("id").description("The id of the foo to update")),
+ responseFields(fieldWithPath("id").description("The id of the updated foo" + collectionToDelimitedString(desc.descriptionsForProperty("id"), ". ")),
+ fieldWithPath("title").description("The title of the updated foo"), fieldWithPath("body").description("The body of the updated foo"))));
+ }
+
+
+}