kagamihogeの日記

kagamihogeの日記です。

DGS Frameworkのチュートリアルレベルをやる

DGS FrameworkNetflixのGraphQLサーバのためのspring-bootベースのフレームワークhttps://netflix.github.io/dgs/getting-started/チュートリアルをやる。

やったこと

build.gradle

https://start.spring.io/ でベースを作ったあとにチュートリアルのページに書いてある依存性を追加する。

plugins {
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("com.netflix.graphql.dgs:graphql-dgs-platform-dependencies:latest.release"))
    implementation("com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter")

    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

GraphQLのスキーマを追加する。デフォルトではsrc/main/resources/schemaスキーマファイルを作るので、src/main/resources/schema/schema.graphqls を作成する。スキーマの中身はこのチュートリアルではなく https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.graphql.schema からコピペした。

type Query {
    greeting(name: String! = "Spring"): String!
    project(slug: ID!): Project
}

""" A Project in the Spring portfolio """
type Project {
    """ Unique string id used in URLs """
    slug: ID!
    """ Project name """
    name: String!
    """ URL of the git repository """
    repositoryUrl: String!
    """ Current support status """
    status: ProjectStatus!
}

enum ProjectStatus {
    """ Actively supported by the Spring team """
    ACTIVE
    """ Supported by the community """
    COMMUNITY
    """ Prototype, not officially supported yet  """
    INCUBATING
    """ Project being retired, in maintenance mode """
    ATTIC
    """ End-Of-Lifed """
    EOL
}

エントリーポイントのmainを作る。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BootGraphqlApplication {
  public static void main(String[] args) {
    SpringApplication.run(BootGraphqlApplication.class, args);
  }
}

スキーマのtypeとenumに対応するデータ入れるためのクラスを作る。

public record Project(String slug, String name, String repositoryUrl, ProjectStatus status) {}

public enum ProjectStatus {
  ACTIVE, COMMUNITY, INCUBATING, ATTIC, EOL
}

queryを実装する。Spring for GraphQLと異なり@Controllerは要らない。

import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsQuery;
import com.netflix.graphql.dgs.InputArgument;

@DgsComponent
public class ProjectDataFetcher {
  @DgsQuery
  public String greeting(@InputArgument String name) {
    return "Hello, " + name + "!";
  }

  @DgsQuery
  public Project project(@InputArgument String slug) {
    return new Project(slug, "asdf", "sadf", ProjectStatus.ACTIVE);
  }
}

起動したら http://localhost:8080/graphiql でGraphQLのUIにアクセスできる。

参考URL

Spring for GraphQLのチュートリアルレベルをやる

Spring for GraphQLチュートリアルレベルの事をやる。

やったこと

hello world相当

https://start.spring.io/ でgraphql, webの依存性を追加する。build.gradleは下記となる。

plugins {
    id 'org.springframework.boot' version '2.7.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-graphql'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework:spring-webflux'
    testImplementation 'org.springframework.graphql:spring-graphql-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

src/main/resources/application.propertiesに以下を任意で追加する。GraphQLはクライアントが必要だがこのプロパティによりブラウザでGraphQLを試せる。

spring.graphql.graphiql.enabled true

GraphQLのスキーマを追加する。デフォルトではsrc/main/resources/graphql/**.graphqls.gqlsの拡張子でスキーマファイルを作るので、src/main/resources/graphql/schema.graphqlsを作成する。スキーマの中身は https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.graphql.schema をまずはコピペする。

type Query {
    greeting(name: String! = "Spring"): String!
    project(slug: ID!): Project
}

""" A Project in the Spring portfolio """
type Project {
    """ Unique string id used in URLs """
    slug: ID!
    """ Project name """
    name: String!
    """ URL of the git repository """
    repositoryUrl: String!
    """ Current support status """
    status: ProjectStatus!
}

enum ProjectStatus {
    """ Actively supported by the Spring team """
    ACTIVE
    """ Supported by the community """
    COMMUNITY
    """ Prototype, not officially supported yet  """
    INCUBATING
    """ Project being retired, in maintenance mode """
    ATTIC
    """ End-Of-Lifed """
    EOL
}

まずはgreetingのQueryを作る。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

@Controller
@SpringBootApplication
public class BootGraphqlApplication {
  public static void main(String[] args) {
    SpringApplication.run(BootGraphqlApplication.class, args);
  }

  @QueryMapping
  public String greeting(@Argument String name) {
    return "Hello, " + name + "!";
  }
}

これを起動して http://localhost:8080/graphiql にアクセスする。下記のようなqueryを書いて実行すると結果が返ってくる。

query {
  greeting(name:"aaaaaaaabbbb")
}

次にprojectのQueryを作る。ProjectProjectStatusを作り、

public record Project(String slug, String name, String repositoryUrl, ProjectStatus status) {
}
public enum ProjectStatus {
  ACTIVE, COMMUNITY, INCUBATING, ATTIC, EOL
}

projectのQueryの本体はこんな感じ。

  @QueryMapping
  public Project project(@Argument String slug) {
    return new Project(slug, "asdf", "sadf", ProjectStatus.ACTIVE);
  }

http://localhost:8080/graphiql にアクセスしてqueryを実行すると以下のようになる。name, statusのみ指定してるのでそのフィールドしか返ってきてないのが分かる。

query {
  greeting(name:"testeset")
  
  project(slug:"hatenatena") {
    name,
    status
  }
}

Spring MVC以外で公開

上記はSpring MVCを使用したが、GraqhQLはそれらとは独立している。https://docs.spring.io/spring-boot/docs/current/reference/html/web.html#web.graphql.schema によると、下記が挙げられている。

  • spring-boot-starter-web
  • spring-boot-starter-websocket
  • spring-boot-starter-webflux
  • spring-boot-starter-rsocket

webfluxを試す。依存性にgraphql, webfluxを追加したものに変更する。

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-graphql'
  implementation 'org.springframework.boot:spring-boot-starter-webflux'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'io.projectreactor:reactor-test'
  testImplementation 'org.springframework.graphql:spring-graphql-test'
}

とりあえずMonoを返すようにgreetingを修正する。

  @QueryMapping
  public Mono<String> greeting(@Argument String name) {
    return Mono.fromCallable(() -> "Hello, " + name + "!");
  }

あとは特に変わらず http://localhost:8080/graphiql からアクセスできる。

PostmanをGraqhQLクライアントとして使用

PostmanをGraqhQLクライアントとして使用する。

GraphQLのエンドポイントに POST http://localhost:8080/graphql注意 graphiqlではない)にアクセスする。BodyにGraphQLを選択してRefreshアイコンをクリックすれば後の使い勝手はgraphiqlと似たような感じ。

参照ドキュメント

WebTestClientによるintegration testの記述

spring-bootで作成するweb-apiのintegration test(以下IT)を記述するのにWebTestClientが使える。これは内部的にWebClientを使用する。同様にTestRestTemplateは内部的にRestTemplateを使用する。環境に応じて使い分けになる。今回はWebTestClientによるITの記述について。

ドキュメントは WebTestClient :: Spring Docsにあり、ここ読めばだいたいは困らないと思う。

ソースコード

build.gradle

plugins {
  id 'org.springframework.boot' version '2.6.7'
  id 'io.spring.dependency-management' version '1.0.11.RELEASE'
  id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
}

repositories {
  mavenCentral()
}

dependencies {
  implementation 'org.springframework.boot:spring-boot-starter-web'
  compileOnly 'org.projectlombok:lombok'
  developmentOnly 'org.springframework.boot:spring-boot-devtools'
  annotationProcessor 'org.projectlombok:lombok'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
}

tasks.named('test') {
  useJUnitPlatform()
}

注意点として、WebTestClientのinterfaceはspring-testにいるがWebClientの実クラスがないと実行時エラーになる。ので、依存性にwebfluxが必要になる。webfluxの依存性はtestImplementationにしているがこれは従来のwebでのITを想定してるため。webfluxを使うならtestにする必要はない。

controller

まず適当なcontrollerを作る。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication()
public class SampleWebTestClientApplication {
  public record SampleResponse(String first, String second, List<Integer> list) {
  }

  @RequestMapping("/sample")
  public SampleResponse sample() {
    return new SampleResponse("f", "s", List.of(1, 2, 3));
  }

  public static void main(String[] args) {
    SpringApplication.run(SampleWebTestClientApplication.class, args);
  }
}

test

テストクラスを作る。

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTest {
  @LocalServerPort
  int port;
  
  @Autowired
  WebTestClient client;
  
  @Test
  void test() {
    String json = """
        {
          "second":"s",
          "first":"f"  ,
          
          "list":[3,2,1]
        }
        """;
    client.get().uri("/sample")
      .exchange()
      .expectStatus().isOk()
      .expectBody().json(json);
    }
}
  • @SpringBootTestは今まで通りのapiを実際に起動してのテストを行うためのアノテーション
  • WebTestClient@SpringBootTestによって自動的にbeanが作られる。どうゆうbeanが作られるかはここを参照。色々設定してなければ、 WebTestClient.bindToServer().baseUrl("http://localhost:" + port).build()とおおむね同様と見てよい。
  • apiにアクセスしてレスポンスのjson文字列同士を比較している。javaのクラスにdeserializeしてから比較しても良いと思う。ただ、デフォルトで割と柔軟なjsonチェックをしてくれるので、ITとしてはこれで良いんじゃないかな、とか思う。
  • 柔軟なチェックの概要はソースコードにもある通り、空白や改行・要素順序・配列の要素順序、を無視してくれる。また、expect側に要素が欠けててもokになる(ex {"second":"s"}でもok)。strictなチェックをしたい場合は.expectBody().json(json, true)にする*1
  • WebTestClientに色々あるassertでは不足していてカスタマイズしたい場合はconsumeWithを使う。以下はjson(expect)とほぼ同等の挙動になる。
    client.get().uri("/sample")
      .exchange()
      .expectStatus().isOk()
      .expectBody(String.class)
      .consumeWith(result -> {
        try {
          new JsonExpectationsHelper().assertJsonEqual(json, result.getResponseBody(), false);
        } catch (Exception e) {
          e.printStackTrace();
        }
      });

*1:5.3.16以降で指定可能