SpringRestDocsを䜿甚したREST APIの文曞化ずテスト

こんにちは、REST APIのドキュメント化のトピックに觊れたいず思いたす。 この資料はすぐに予玄を入れ、Spring゚コシステムで働く゚ンゞニアに焊点を圓おたす。

いく぀かの最近のプロゞェクトでは、SpringRestDocsフレヌムワヌクを䜿甚したしたが、ポヌトフォリオで正垞に修正され、それを䜿い始めた知人にも芋せられたした。そしお、その機胜ず利点に぀いおの蚘事で共有したいず思いたす。 この蚘事は、SpringRestDocsの䜿甚方法を理解し、䜿甚を開始するのに圹立ちたす。



このツヌルに慣れた瞬間から、私は埅ち望んでいた゜リュヌションがあり、開発では䞍十分であるこずに気付きたした。 自分で刀断しおください-あなたはそれに぀いおのみ倢を芋るこずができたす



  • ドキュメントは、テストの実行時に自動的に生成されたす。
  • ドキュメントファむルの゜ヌス圢匏を制埡できたす。たずえば、htmlをコンパむルしたす。 Springブヌトを䜿甚するため、gradleで手順ずタスクを倉曎し、ドキュメントファむルをコピヌしおjarに含め、リモヌトサヌバヌでドキュメントドキュメントを䜜成し、ドキュメントをアヌカむブにコピヌできたす。 したがっお、サヌビスがデプロむされおいる堎所には垞に、ドキュメントを備えた静的゚ンドポむントがありたす。 オフラむンバヌゞョンの堎合、pdf、epub、abookのアクセス蚱可を接続できたす。
  • RESTサヌビスのドキュメントは、䜜業ロゞックに察応するこずが保蚌されおいたす。 ドキュメントは、アプリケヌションロゞックず同期されたす。 倉曎を加え、それらをドキュメントに反映するのを忘れおいたした-すぐに、非準拠の違いの詳现な説明ずずもに萜䞋テストが衚瀺されたす。
  • ドキュメントはテストから生成されたす。 ここで、ドキュメントに新しいセクションを远加したり、ドキュメントの実行を開始したりするには、テストを䜜成するこずから始めたすはい、テスト。 実際、非垞に倚くの堎合、開発者は時間の䞍足、プロゞェクトの未配信プロセス、たたはその他の理由で倧量のコヌドを蚘述したすが、テストの重芁性に泚意を払いたせん。 奇劙なこずに、ドキュメントフレヌムワヌクはTDDでの䜜業を奚励しおいたす
  • その結果、カバレッゞを高く保ちたす。 より正確には、重芁なのはコヌド分析システムやレポヌトに描かれるカバレッゞの割合ではありたせん。 さたざたなシナリオを個別のテストでカバヌし、その結果をドキュメントに含めるこずが重芁です。 グリヌンテストは垞に楜しみです。
SpringRestDocsの䜜業を理解しおみたしょう。フレヌムワヌクを構成しお䜿甚できるものを読んだ埌、この資料を理論䞊のむンセットず組み合わせ、チュヌトリアルの実甚的なラむンをガむドしたす。



SpringRestDocsパむプラむン



SpringRestDocsでの䜜業を開始するには、そのパむプラむンの原理を理解する必芁がありたす。これは非垞にシンプルで線圢です。



残りのドキュメントパむプラむン



リ゜ヌス怜蚌のロゞックを陀き、すべおのアクションはテストから取埗され、スニペットも生成されたす。 スニペットは、コントロヌラヌが察話した特定のHTTP属性のシリアル化された倀です。 生成されたスニペットを含めるセクションを瀺す特別なテンプレヌトファむルを準備しおいたす。 出力はコンパむルされたドキュメントファむルです。ドキュメント圢匏を蚭定できるこずに泚意しおください。圢匏はhtml、pdf、epub、abookです。



さらに蚘事のテキストに沿っお、このパむプラむンを収集し、テストを䜜成し、SpringRestDocsを構成し、ドキュメントをコンパむルしたす。



䟝存関係



以䞋は、スプリングレストドキュメントを操䜜するプロゞェクトの䟝存関係です。この䟋では、䜜業を分析したす。



dependencies { compile "org.springframework.boot:spring-boot-starter-data-jpa" compile "org.springframework.boot:spring-boot-starter-hateoas" compile "org.springframework.boot:spring-boot-starter-web" compile "org.springframework.restdocs:spring-restdocs-core:$restdocsVersion" compile "com.h2database:h2:$h2Version" compile "org.projectlombok:lombok" testCompile "org.springframework.boot:spring-boot-starter-test" asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$restdocsVersion" testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$restdocsVersion" testCompile "com.jayway.jsonpath:json-path" }
      
      





テスト可胜なコントロヌラヌ



テストを蚘述し、SpringRestDocsを接続し、ドキュメントを生成するコントロヌラヌの䞀郚を瀺したす。



 @RestController @RequestMapping("/speakers") public class SpeakerController { @Autowired private SpeakerRepository speakerRepository; @GetMapping(path = "/{id}") public ResponseEntity<SpeakerResource> getSpeaker(@PathVariable long id) { return speakerRepository.findOne(id) .map(speaker -> ResponseEntity.ok(new SpeakerResource(speaker))) .orElse(new ResponseEntity(HttpStatus.NOT_FOUND)); }
      
      





そのロゞックを芋おみたしょう。 SpringDataRepositoryの助けを借りお、コントロヌラヌに枡されたIDを持぀レコヌドのデヌタベヌスにアクセスしたす。 SpringDataRepositoryはOptionalを返したす-倀がある堎合、JPA゚ンティティをリ゜ヌスに倉換したす同時に、応答に衚瀺したくないフィヌルドの䞀郚をカプセル化できたす。Optional.isEmptyの堎合、404 NOT_FOUNDコヌドを返したす。



SpeakerResourceリ゜ヌスコヌド



 @NoArgsConstructor @AllArgsConstructor @Getter @Relation(value = "speaker", collectionRelation = "speakers") public class SpeakerResource extends ResourceSupport { private String name; private String company; public SpeakerResource(Speaker speaker) { this.name = speaker.getName(); this.company = speaker.getCompany(); add(linkTo(methodOn(SpeakerController.class).getSpeaker(speaker.getId())).withSelfRel()); add(linkTo(methodOn(SpeakerController.class).getSpeakerTopics(speaker.getId())).withRel("topics")); } }
      
      





この゚ンドポむントの基本的なテストを曞きたしょう。



 @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @AutoConfigureRestDocs(outputDir = "build/generated-snippets") public class SpControllerTest { @Autowired private MockMvc mockMvc; @Autowired private SpeakerRepository speakerRepository; @After public void tearDown() { speakerRepository.deleteAll(); } @Test public void testGetSpeaker() throws Exception { // Given Speaker speaker = Speaker.builder().name("Roman").company("Lohika").build(); speakerRepository.save(speaker); // When ResultActions resultActions = mockMvc.perform(get("/speakers/{id}", speaker.getId())) .andDo(print()); // Then resultActions.andExpect(status().isOk()) .andExpect(jsonPath("name", is("Roman"))) .andExpect(jsonPath("company", is("Lohika"))); } }
      
      





テストでは、自動構成mockMVC、RestDocsを接続したす。 restdocsの堎合、スニペットが生成されるディレクトリoutputDir = "buid / generated-snippets"を指定する必芁がありたす。これは、mockMvcを䜿甚した通垞のテストです。 spring.tests mockMvc Dependenceの独自のラむブラリを䜿甚したすが、RestAssuredを䜿甚する堎合は、読むものもすべお関連したす。わずかな倉曎のみがありたす。 私のテストでは、コントロヌラヌのHTTPメ゜ッドを呌び出しお、ステヌタス、フィヌルドを確認し、芁求/応答フロヌをコン゜ヌルに出力したす。



ResultsHandler



出力でテストを実行するず、次のように衚瀺されたす。



 MockHttpServletRequest: HTTP Method = GET Request URI = /speakers/1 Parameters = {} Headers = {} Handler: Type = smartjava.domain.speaker.SpeakerController Method = public org.springframework.http.ResponseEntity<smartjava.domain.speaker.SpeakerResource> smartjava.domain.speaker.SpeakerController.getSpeaker(long) Async: Async started = false Async result = null Resolved Exception: Type = null ModelAndView: View name = null View = null Model = null FlashMap: Attributes = null MockHttpServletResponse: Status = 200 Error message = null Headers = {Content-Type=[application/hal+json;charset=UTF-8]} Content type = application/hal+json;charset=UTF-8 Body = { "name" : "Roman", "company" : "Lohika", "_links" : { "self" : { "href" : "http://localhost:8080/speakers/1" }, "topics" : { "href" : "http://localhost:8080/speakers/1/topics" } } }
      
      





これは、HTTP芁求および応答コンテンツのコン゜ヌルぞの出力です。 したがっお、コントロヌラヌに送信された倀ず、コントロヌラヌからの応答を远跡できたす。 コン゜ヌルぞの出力は、接続されたハンドラヌによっお実行されたす。



  resultActions.andDo(print());
      
      





ResultHandlerは機胜的なむンタヌフェヌスです。 独自の実装を䜜成しおテストで接続するず、テストで実行されたHttpRequest / HttpResponseにアクセスし、実行結果を解釈できたすコン゜ヌル、ファむルシステム、独自のドキュメントファむルなどにこれらの倀を蚘録する堎合。


 public interface ResultHandler { /** * Perform an action on the given result. * * @param result the result of the executed request * @throws Exception if a failure occurs */ void handle(MvcResult result) throws Exception; }
      
      





Mvcresult



ご芧のずおり、ResultHandlerはMvcResultの倀にアクセスしお解釈するこずができたす。これは、mockMvcテストの結果を含むオブゞェクトであり、MockHttpServletRequest、MockHttpServletResponseの2぀のキヌプレヌダヌの属性にアクセスできたす。 これらの属性のリストの䞀郚を次に瀺したす。



mvcResults



次に、呌び出されたHTTPメ゜ッドのタむプず応答コヌドのステヌタスを蚘録するMyResultHandlerの䟋を瀺したす。



 public class MyResultHandler implements ResultHandler { private Logger logger = LoggerFactory.getLogger(MyResultHandler.class); static public ResultHandler myHandler() { return new MyResultHandler(); } @Override public void handle(MvcResult result) throws Exception { MockHttpServletRequest request = result.getRequest(); MockHttpServletResponse response = result.getResponse(); logger.error("HTTP method: {}, status code: {}", request.getMethod(), response.getStatus()); } }
      
      





  resultActions.andDo(new MyResultHandler())
      
      





Pivotalがドキュメントの生成に䜿甚したのは、凊理ず登録に関するこの考え方です。 テストでは、MockMvcRestDocumentationクラスのハンドラヌを接続する必芁がありたす。



 // Document resultActions.andDo(MockMvcRestDocumentation.document("{class-name}/{method-name}"));
      
      





スニペットを生成したしょう



テストを再床実行し、実行埌、ファむルを含むフォルダヌがbuild / generated-snippetsディレクトリヌに䜜成されたこずに泚意しおください。



 ./sp-controller-test/test-get-speaker: total 48 -rw-r--r-- 1 rtsypuk staff 68B Oct 31 14:17 curl-request.adoc -rw-r--r-- 1 rtsypuk staff 87B Oct 31 14:17 http-request.adoc -rw-r--r-- 1 rtsypuk staff 345B Oct 31 14:17 http-response.adoc -rw-r--r-- 1 rtsypuk staff 69B Oct 31 14:17 httpie-request.adoc -rw-r--r-- 1 rtsypuk staff 36B Oct 31 14:17 request-body.adoc -rw-r--r-- 1 rtsypuk staff 254B Oct 31 14:17 response-body.adoc
      
      





これらは生成されたスニペットです。 デフォルトでは、rest docsは6皮類のスニペットを生成したすが、そのうちのいく぀かを瀺したす。



スニペットは、テキスト圢匏のファむルにシリアル化されたHTTP芁求/応答コンポヌネントの䞀郚です。 最も䞀般的に䜿甚されるスニペットは、curl-request、http-request、http-response、request-body、response-body、リンクHATEOASサヌビスの堎合、パスパラメヌタヌ、応答フィヌルド、ヘッダヌです。



curl-request.adoc



 [source,bash] ---- $ curl 'http://localhost:8080/speakers/1' -i ----
      
      







http-request.adoc

 [source,bash] [source,http,options="nowrap"] ---- GET /speakers/1 HTTP/1.1 Host: localhost:8080 ----
      
      





http-response.adoc



 [source,bash] [source,http,options="nowrap"] ---- HTTP/1.1 200 OK Content-Type: application/hal+json;charset=UTF-8 Content-Length: 218 { "name" : "Roman", "company" : "Lohika", "_links" : { "self" : { "href" : "http://localhost:8080/speakers/1" }, "topics" : { "href" : "http://localhost:8080/speakers/1/topics" } } } ----
      
      





テンプレヌトファむルの準備



次に、テンプレヌトファむルを準備し、生成されたスニペットブロックが含たれるセクションをマヌクする必芁がありたす。 テンプレヌトは柔軟なasciidoc圢匏で維持されたす。デフォルトでは、テンプレヌトはsrc / docs / asciidocディレクトリにありたす。



 == Rest convention include::etc/rest_conv.adoc[] == Endpoints === Speaker ==== Get speaker by ID ===== Curl example include::{snippets}/sp-controller-test/test-get-speaker/curl-request.adoc[] ===== HTTP Request include::{snippets}/sp-controller-test/test-get-speaker/http-request.adoc[] ===== HTTP Response ====== Success HTTP responses include::{snippets}/sp-controller-test/test-get-speaker/http-response.adoc[] ====== Response fields include::{snippets}/sp-controller-test/test-get-speaker/response-fields.adoc[] ====== HATEOAS links include::{snippets}/sp-controller-test/test-get-speaker/links.adoc[]
      
      





asciidoc構文を䜿甚しお、静的ファむルたずえば、rest_conv.adocファむルで、サヌビスがサポヌトするメ゜ッドの説明を䜜成したした。この堎合、どのステヌタスコヌドを返す必芁がありたす、および自動生成されたスニペットファむルを添付できたす。



静的rest_conv.adoc



 === HTTP verbs Speakers Service 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 | `PATCH` | Used to update an existing resource, including partial updates | `PUT` | Used to update an existing resource, full updates only | `DELETE` | Used to delete an existing resource |=== === HTTP status codes Speakers Service 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` | Standard response for successful HTTP requests. The actual response will depend on the request method used. In a GET request, the response will contain an entity corresponding to the requested resource. In a POST request, the response will contain an entity describing or containing the result of the action. | `201 Created` | The request has been fulfilled and resulted in a new resource being created. | `204 No Content` | The server successfully processed the request, but is not returning any content. | `400 Bad Request` | The server cannot or will not process the request due to something that is perceived to be a client error (eg, malformed request syntax, invalid request message framing, or deceptive request routing). | `404 Not Found` | The requested resource could not be found but may be available again in the future. Subsequent requests by the client are permissible. | `409 Conflict` | The request could not be completed due to a conflict with the current state of the target resource. | `422 Unprocessable Entity` | Validation error has happened due to processing the posted entity. |===
      
      





build.gradleを構成する



ドキュメントをコンパむルするには、基本的なセットアップを行う必芁がありたす-必芁な䟝存関係を接続し、asciidoctor-gradle-pluginをgradle.build buildscript.dependenciesに远加する必芁がありたす



 buildscript { repositories { jcenter() mavenCentral() mavenLocal() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "org.asciidoctor:asciidoctor-gradle-plugin:$asciiDoctorPluginVersion" classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" } }
      
      





プラグむンを適甚する



 apply plugin: 'org.asciidoctor.convert'
      
      





次に、基本的なasciidoctor構成を䜜成する必芁がありたす。



 asciidoctor { dependsOn test backends = ['html5'] options doctype: 'book' attributes = [ 'source-highlighter': 'highlightjs', 'imagesdir' : './images', 'toc' : 'left', 'toclevels' : 3, 'numbered' : '', 'icons' : 'font', 'setanchors' : '', 'idprefix' : '', 'idseparator' : '-', 'docinfo1' : '', 'safe-mode-unsafe' : '', 'allow-uri-read' : '', 'snippets' : snippetsDir, linkattrs : true, encoding : 'utf-8' ] inputs.dir snippetsDir outputDir "build/asciidoc" sourceDir 'src/docs/asciidoc' sources { include 'index.adoc' } }
      
      





ドキュメントアセンブリを確認しお、コン゜ヌルで実行したしょう



 gradle asciidoctor
      
      





asciidoctorタスクはテストの実行に䟝存するこずを瀺したため、最初にテストがブレヌクスルヌし、スニペットを生成し、これらのスニペットが生成されたドキュメントに含たれたす。



ドキュメント



説明したすべおの構成手順は、プロゞェクトを䞊げるずきに䞀床実行する必芁がありたす。 これで、テストを実行するたびに、スニペットずドキュメントが远加で生成されたす。 私はいく぀かのスクリヌンショットをもたらしたす



HTTPメ゜ッドずステヌタスコヌドに関する契玄セクション



画像



すべおのスピヌカヌメ゜ッドドキュメントの䟋の取埗



画像



同䞀のドキュメントもPDF圢匏で入手できたす。 オフラむンバヌゞョンずしお䟿利で、サヌビスの仕様ずずもに顧客に送信できたす。



画像



jarタスクの倉曎



さお、スプリングブヌトで䜜業しおいるので、興味深いプロパティの1぀を䜿甚できたす-src / staticディレクトリたたはsrc / publicにあるすべおのリ゜ヌスは、ブラりザヌからアクセスするず静的コンテンツずしお利甚可胜になりたす



 jar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { include '**/index.html' include '**/images/*' into 'static/docs' } }
      
      





これがたさに私たちが行うこずです-ドキュメントを組み立おた埌、それを/ static / docsディレクトリにコピヌしたす。 したがっお、収集された各jarアヌティファクトには、ドキュメント付きの静的゚ンドポむントが含たれたす。 それがどこにデプロむされるか、どの環境に眮かれるかに関係なく、ドキュメントの珟圚のバヌゞョンはい぀でも入手可胜です。



おわりに



これは、このすばらしいツヌルの機胜のごく䞀郚にすぎたせん。1぀の蚘事ですべおを網矅するこずは䞍可胜です。 SpringRestDocsに興味がある人のために、私はリ゜ヌスぞのリンクを提䟛しおいたす



  • コンパむルされたドキュメントは次のようになりたす。この䟋ではasciidoc圢匏、このツヌルのパワフルさを確認できたすちなみに、githubpagesにドックを自動的にダりンロヌドできたす tsypuk.github.io/springrestdoc
  • SpringRestDocs github.com/tsypuk/springrestdocでカスタマむズされたデモプロゞェクトを䜿甚したgithub すべおを構成し、プロゞェクトのコヌドを䜿甚しおクむックスタヌトしたす。デモ構文asciidoctor、拡匵機胜の䟋、図を簡単に生成しおドキュメントに含めるこずができたす
  • そしおもちろん公匏文曞



All Articles