【WIP】JUnit(REST Assured)でQuarkus製アプリをデバッグ実行するとタイムアウトする問題を解決した

検証環境

  • JUnit 5.10.2
  • REST Assured 5.4.0
  • Java 17.0.5

事象

JUnit(REST Asssured)を使ったテストデバッグでステップ実行を行っていると、 java.net.SocketTimeoutException: Read timed outが発生した。

解決方法

@QuarkusTest
public class ApiTest {
    @Test
    public void testHelloEndpoint() {

        given()
                .config(RestAssured.config()
                        .httpClient(HttpClientConfig.httpClientConfig()
                                .setParam("http.socket.timeout", 1000000)))
                .when()
                .log().all()
                .get("hogehoge-service/hogehoge-service/sampleA/hello")
                .then()
                .log().all()
                .statusCode(200);
    }

}

参考

【Quarkus】OpenAPI Generator(jaxrs-spec)で生成したソースが、エンドポイントがうまく指定できずREST Assuredでテストできない

TL;DR

  • 生成対象となるOpenAPIでpathを1種類しか持たないとき、
    • クラス単位に設定される@Pathにフルパスが設定される
    • メソッド単位にパスは設定されない
  • Quarkusが提供するテスト用アノテーション@TestHTTPResourceは、クラス単位に設定された@Pathまでを注入する
  • したがって、クラス単位に設定したPathにパスパラメータが含まれている場合、@TestHTTPResourceを利用したテストができない
  • @TestHTTPResourceを外してフルパスでテストするしかなさそう

環境

動作環境

  • Ubuntu 20.04(WSL2)
  • openapitools/openapi-generator-cli(Dockerイメージ)

設定値

library: quarkus
additionalProperties:
  dateLibrary: java8-localdatetime
  hideGenerationTimestamp: true
  openApiNullable: false
  useBeanValidation: true
  useRuntimeException: true
  microprofileRestClientVersion: "3.0"
  serializationLibrary: "jackson"
  useJakartaEe: true
  generateBuilders: true
  interfaceOnly: true
  useSwaggerAnnotations: false
  sourceFolder: "src/main/java"

実行コマンドの例

GENERATOR=jaxrs-spec && docker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli generate -i /local/input/petstore_2path.yaml -g ${GENERATOR} -o /local/out2/${GENERATOR} -c /local/input/config_server.yaml

試したこと(エンドポイントの個数によって生成のされ方が異なる)

エンドポイントが1種類のとき

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets/{id}:
    post:
      summary: "Add a new pet to the store"
      description: "desc"
      operationId: "methodpost"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PetsResponse"
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      age:
                        type: string
                      sex:
                        type: string
    put:
      summary: "Add a new pet to the store"
      description: "desc"
      operationId: "methodput"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PetsResponse"
                type: object
components:
  schemas:
    Petfoo:
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Petshoge:
      type: array
      items:
        $ref: "#/components/schemas/Petfoo"

    PetsResponse:
      properties:
        id:
          type: integer
          format: int64
          example: 3
        name:
          type: string
          example: "wan"
        tag:
          type: string
          example: "inu"
    Error:
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
package org.openapitools.api;

import org.openapitools.model.PetsResponse;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;




import java.io.InputStream;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;


@Path("/pets/{id}")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.5.0-SNAPSHOT")
public interface PetsApi {

    @POST
    @Produces({ "application/json" })
    PetsResponse methodpost(@PathParam("id") Integer id);

    @PUT
    @Produces({ "application/json" })
    PetsResponse methodput(@PathParam("id") Integer id);
}

エンドポイントが2種類のとき

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets/{id}:
    post:
      summary: "Add a new pet to the store"
      description: "desc"
      operationId: "methodpost"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PetsResponse"
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      age:
                        type: string
                      sex:
                        type: string
    put:
      summary: "Add a new pet to the store"
      description: "desc"
      operationId: "methodput"
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: ユーザID
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PetsResponse"
                type: object
  /pets/name/{name}:
    post:
      summary: "Add a new pet to the store"
      description: "desc"
      operationId: "anothermethodpost"
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: integer
          description: 顧客名
      responses:
        200:
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PetsResponse"
                type: object
                properties:
                  user:
                    type: object
                    properties:
                      age:
                        type: string
                      sex:
                        type: string
components:
  schemas:
    Petfoo:
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Petshoge:
      type: array
      items:
        $ref: "#/components/schemas/Petfoo"

    PetsResponse:
      properties:
        id:
          type: integer
          format: int64
          example: 3
        name:
          type: string
          example: "wan"
        tag:
          type: string
          example: "inu"
    Error:
      required:
        - code
        - message
      properties:
        code:
          type: integer
          format: int32
        message:
          type: string
package org.openapitools.api;

import org.openapitools.model.PetsResponse;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;




import java.io.InputStream;
import java.util.Map;
import java.util.List;
import jakarta.validation.constraints.*;
import jakarta.validation.Valid;


@Path("/pets")
@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", comments = "Generator version: 7.5.0-SNAPSHOT")
public interface PetsApi {

    @POST
    @Path("/name/{name}")
    @Produces({ "application/json" })
    PetsResponse anothermethodpost(@PathParam("name") Integer name);

    @POST
    @Path("/{id}")
    @Produces({ "application/json" })
    PetsResponse methodpost(@PathParam("id") Integer id);

    @PUT
    @Path("/{id}")
    @Produces({ "application/json" })
    PetsResponse methodput(@PathParam("id") Integer id);
}

参考

Vue.js(Vite)のDockerコンテナを起動したとき、「Error: EACCES: permission denied」エラーが発生したのを解決した

実行環境

  • WSL2(Ubuntu20.04)
  • Docker
  • node.js v21.0.0 (WSL2)
  • node.js v16.17.1

事象

docker composeを使ってDockerコンテナでVue.js(Vite)を起動したとき、次のようなエラーが出た。 (WSLから直接viteを起動することはできている)

root@4ce1c6e38951:/src# npm run dev

> vue-project@0.0.0 dev
> vite

error when starting dev server:
Error: EACCES: permission denied, rmdir '/src/node_modules/.vite/deps'

解決方法

Dockerコンテナに入ってrm rf node_modules/ npm installしたところ解決した

  • 根本的な理由は不明
  • WSL側から同コマンドを実行しても解決しなかった

諸々試したこと

  • WSL側から/src/node_modules/.vite/depsの権限を777に変更してみた(Dockerコンテナからも反映されていることを確認できたが解決には至らず)
  • WSL側からnode_modulesを削除、npm installしたが解決には至らず

PostgreSQLで存在するはずのテーブル定義の確認を試みると"Did not find any relation named ... "エラーとなる問題を解決した

事象

\dtコマンドで確認すると確かに存在するUserテーブルが、\d Userコマンドでテーブル定義を確認できず、エラーとなった。

postgres=# \dt
              List of relations
 Schema |        Name        | Type  | Owner 
--------+--------------------+-------+-------
 public | User               | table | user
 public | _prisma_migrations | table | user
 public | learning_list      | table | user
(3 rows)

postgres=# \d User
Did not find any relation named "User".

環境

PostgreSQL 14(docker image; postgres:14)

原因

PostgreSQLでは、ダブルクォーテーションで囲んで明示的に指示しない限り、大文字は小文字に解釈されるため、 テーブル名を小文字のみ構成にするか、ダブルクォーテーションで\d "Usersのように指定する

引用符が付かない名前は常に小文字に解釈されますが、識別子を引用符で囲むことによって大文字と小文字が区別されるようになります。 例えば、識別子FOO、foo、"foo"はPostgreSQLによれば同じものとして解釈されますが、"Foo"と"FOO"は、これら3つとも、またお互いに違ったものとして解釈されます (PostgreSQLが引用符の付かない名前を小文字として解釈することは標準SQLと互換性がありません。標準SQLでは引用符の付かない名前は大文字に解釈されるべきだとされています。 したがって標準SQLによれば、fooは"FOO"と同じであるべきで、"foo"とは異なるはずなのです。 もし移植可能なアプリケーションを書きたいならば、特定の名前は常に引用符で囲むか、あるいはまったく囲まないかのいずれかに統一することをお勧めします。)

PostgreSQL 14.5文書 第4章 SQLの構文 4.1.1.識別子とキーワード

サンプルコードをほぼ転記して動作確認したいだけのフェーズで発生した事象のため、テーブル名を小文字で作り直す修正方針でよさそう。

参考

Mockitoの検証のため環境構築をした

検証環境

  • Ubuntu 20.04(WSL2)
  • Java 17
  • SDKMANはインストールされているものとする

次のコマンドでgradleを導入 $ sdk install gradle

$ gradle -v

------------------------------------------------------------
Gradle 8.7
------------------------------------------------------------

Build time:   2024-03-22 15:52:46 UTC
Revision:     650af14d7653aa949fce5e886e685efc9cf97c10

Kotlin:       1.9.22
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          17.0.5 (Eclipse Adoptium 17.0.5+8)
OS:           Linux 5.10.16.3-microsoft-standard-WSL2 amd64

検証したいディレクトリで$ gradle initを実行

gradle init

Select type of build to generate:
  1: Application
  2: Library
  3: Gradle plugin
  4: Basic (build structure only)
Enter selection (default: Application) [1..4] 1

Select implementation language:
  1: Java
  2: Kotlin
  3: Groovy
  4: Scala
  5: C++
  6: Swift
Enter selection (default: Java) [1..6] 1

Enter target Java version (min: 7, default: 21): 17

Project name (default: study_mockito2): 

Select application structure:
  1: Single application project
  2: Application and library project
Enter selection (default: Single application project) [1..2] 1

Select build script DSL:
  1: Kotlin
  2: Groovy
Enter selection (default: Kotlin) [1..2] 2

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 4

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] no


> Task :init
To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.7/samples/sample_building_java_applications.html

build.gradleの設定はこの記事の内容にしたがった

Mockito 3 + JUnit 5 で基本的なモック化とテストをするサンプルコード

plugins {
  id 'java'
}

repositories {
  jcenter()
}

dependencies {
  // Junit 5 を導入
  // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter
  testImplementation 'org.junit.jupiter:junit-jupiter:5.7.0'

  // Mockito 3 を導入
  // https://mvnrepository.com/artifact/org.mockito/mockito-core
  testImplementation 'org.mockito:mockito-core:3.6.0'

  // Mockito による JUnit 5 Extension ライブラリを導入
  // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter
  testImplementation 'org.mockito:mockito-junit-jupiter:3.6.0'
}

test {
  // JUnit platform を使う設定
  useJUnitPlatform()

  // テスト時の出力設定
  testLogging {
    // テスト時の標準出力と標準エラー出力を表示する
    showStandardStreams true
    // イベントを出力する (TestLogEvent)
    events 'started', 'skipped', 'passed', 'failed'
    // 例外発生時の出力設定 (TestExceptionFormat)
    exceptionFormat 'full'
  }
}

ExpressValidator+Prisma(PostgreSQL)でinput contains invalid characters. Expected ISO-8601 DateTimeエラーを回避するには

ExpressValidator+Prismaを連携させて、PostgreSQLのTIMESTAMP型のカラムへ値を代入しようとした。 また、schema.prisma上はDateTime型である。

RFC3339型(≒ISO8601型)でないと受け付けてくれないため、食べさせる文字列には気を使う必要がある。

ExpressValidatorに適用するカスタムルールはこんな感じでよさそう。

function isRFC3339DateTime(value: string) {
    if (!value) {
        return false;
    }
    try {
        const d = new Date(value);
        d.toISOString();
        return true;
    } catch {
        return false;
    }
}
router.post("/", [body("used_time").isInt({ min: 3, max: 10 }), body("study_date").custom((value: string) => commonUtil.isRFC3339DateTime(value))], async (req: Request, res: Response) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    const result = await memoService.register(req)
    res.json({ result })
});

あわせて後で読みたい - RFC 3339 vs ISO 8601

VSCode拡張のREST Clientが便利

最近、個人開発でもREST ClientというHTTP通信を行うVSCode拡張を使っている。

curlコマンドとPostmanの間の立ち位置かなと思っている。

導入コストは curl < REST Client < Postman

利便性はcurl < REST Client < Postman

VSCodeで開発していることもあり、開発途中の疎通確認レベルや、ちょっとデータベースにレコードを追加する程度であれば、これくらいがちょうどよいのかもしれない。

参考