JerseyTest – readEntity с обобщенным типом

Вопрос или проблема

У меня есть универсальный класс, который используется в качестве ответа всеми ресурсами в моем API

public class TestResponse<T> {
    private boolean result;
    private int status;
    private T data;
    // Геттеры и сеттеры
}

У меня есть еще два класса, которые используются для инициализации поля TestResponse.Data из двух разных ресурсов

public class TestResponseData01 {
    private int id;
    // Геттеры и сеттеры
}
public class TestResponseData02 {
    private String message;
    // Геттеры и сеттеры
}

Один ресурс возвращает ответ типа TestResponse<TestResponseData01>, а другой TestResponse<TestResponseData02>

В тестовом классе я создал метод, который выполняет общие тесты ответов всех ресурсов, но я хотел бы вернуть экземпляр TestResponse, чтобы затем выполнить специфические тесты в зависимости от ресурса, который я тестирую.

private <T> TestResponse<T> commonTest(Response response, Class<T> dataType) {
    TestResponse<T> testResponse = response.readEntity(new GenericType<TestResponse<T>>(){});
    // ...
}

Проблема в том, что выражение TestResponse<T> testResponse = response.readEntity(new GenericType<TestResponse<T>>(){}) десериализует JSON ответа, но для поля TestResponse.Data оно создает объект типа java.util.LinkedHashMap

Как я могу выполнить метод readEntity, чтобы поле TestResponse.Data было правильно инициализировано объектом TestResponseData01 или TestResponseData02?

Полный код

pom.xml зависимости

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.source.encoding>
        <lombok.version>1.18.30</lombok.version>
        <jersey.version>2.25.1</jersey.version>
        <junit.version>5.11.2</junit.version>
    </properties>
    <dependencies>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- Jersey -->
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-bean-validation</artifactId>
            <version>${jersey.version}</version>
        </dependency>
        <!-- JUnit -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Тестирование Jersey -->
        <dependency>
            <groupId>org.glassfish.jersey.test-framework</groupId>
            <artifactId>jersey-test-framework-core</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <version>${jersey.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Класс JerseyTest

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@TestInstance(Lifecycle.PER_CLASS)
public class Test_GenericType extends JerseyTest {
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @ToString
    public static class TestResponse<T> {
        private boolean result;
        private int status;
        private T data;
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @ToString
    public static class TestResponseData01 {
        private int id;
    }
    
    @NoArgsConstructor
    @AllArgsConstructor
    @Getter
    @ToString
    public static class TestResponseData02 {
        private String message;
    }
    
    @Path("testData01")
    @Produces(MediaType.APPLICATION_JSON)
    public static class TestResponseData01Resource {
        @GET
        public Response test() {
            TestResponseData01 testResponseData01 = new TestResponseData01(1);
            TestResponse<TestResponseData01> testResponse = new TestResponse<TestResponseData01>(true, Response.Status.OK.getStatusCode(), testResponseData01);
            return Response.status(Response.Status.OK).entity(testResponse).build();
        }
    }
    
    @Path("testData02")
    @Produces(MediaType.APPLICATION_JSON)
    public static class TestResponseData02Resource {
        @GET
        public Response test() {
            TestResponseData02 testResponseData02 = new TestResponseData02("a");
            TestResponse<TestResponseData02> testResponse = new TestResponse<TestResponseData02>(true, Response.Status.OK.getStatusCode(), testResponseData02);
            return Response.status(Response.Status.OK).entity(testResponse).build();
        }
    }
    
    @Override
    protected Application configure() {
        return new ResourceConfig(TestResponseData01Resource.class, TestResponseData02Resource.class);
    }
    
    @BeforeAll
    public void before() throws Exception {
        super.setUp();
    }
    
    @AfterAll
    public void after() throws Exception {
        super.tearDown();
    }
    
    private <T> TestResponse<T> commonTest(Response response, Class<T> dataType) {
        TestResponse<T> testResponse = response.readEntity(new GenericType<TestResponse<T>>(){});
        assertTrue(testResponse.isResult());
        assertEquals(200, testResponse.getStatus());
        return testResponse;
    }
    
    @Test
    void Test_ResponseData01Resource() {
        Response response = target("testData01").request().get();
        TestResponse<TestResponseData01> testResponse = commonTest(response, TestResponseData01.class);
        TestResponseData01 testData = testResponse.getData();
        assertEquals(1, testData.getId());
    }
    
    @Test
    void Test_ResponseData02Resource() {
        Response response = target("testData02").request().get();
        TestResponse<TestResponseData02> testResponse = commonTest(response, TestResponseData02.class);
        TestResponseData02 testData = testResponse.getData();
        assertEquals("a", testData.getMessage());
    }
}

Ответ или решение

Решение проблемы с десериализацией в JerseyTest: генерические типы

Введение

Вас беспокоит проблема корректной десериализации поля data в вашем классе TestResponse, который является обобщенным и применяется в различных ресурсах вашего API. При чтении ответа вы столкнулись с тем, что тип поля data возвращается как объект типа LinkedHashMap, а не как ожидаемый тип, такой как TestResponseData01 или TestResponseData02. Данная статья предлагает решение вашего вопроса, рассматривая, как правильно настроить десериализацию с использованием JerseyTest.

Проблема

Ваша реализация метода commonTest, использующего response.readEntity(new GenericType<TestResponse<T>>() {}), не учитывает тип data. Из-за этого при десериализации JSON ответа структура, соответствующая типам TestResponseData01 и TestResponseData02, теряется.

Решение

Для того чтобы корректно десериализовать ответ JSON с учетом специфики обобщенных типов, необходимо подробнее рассмотреть, как используется GenericType. Вам нужно передать дополнительную информацию о типе data в GenericType, чтобы указывать, какого типа объект ожидается.

Шаги для решения

  1. Создайте специализированный GenericType для каждого теста. Вам потребуется создать анонимный класс, который будет указывать конкретный тип для T.

  2. Обновите метод commonTest следующим образом:

private <T> TestResponse<T> commonTest(Response response, Class<T> dataType) {
    GenericType<TestResponse<T>> genericType = new GenericType<TestResponse<T>>() {
        @Override
        public String getTypeName() {
            return "TestResponse<" + dataType.getSimpleName() + ">";
        }
    };

    TestResponse<T> testResponse = response.readEntity(genericType);

    // Проверки состояния ответа
    assertTrue(testResponse.isResult());
    assertEquals(200, testResponse.getStatus());
    return testResponse;
}
  1. Измените вызовы метода в тестах:

Вам нужно будет выполнить вызов метода commonTest с указанием типа data:

@Test
void Test_ResponseData01Resource() {
    Response response = target("testData01").request().get();
    TestResponse<TestResponseData01> testResponse = commonTest(response, TestResponseData01.class);
    TestResponseData01 testData = testResponse.getData();
    assertEquals(1, testData.getId());
}

@Test
void Test_ResponseData02Resource() {
    Response response = target("testData02").request().get();
    TestResponse<TestResponseData02> testResponse = commonTest(response, TestResponseData02.class);
    TestResponseData02 testData = testResponse.getData();
    assertEquals("a", testData.getMessage());
}

Заключение

Таким образом, для корректной десериализации поля data в вашем классе TestResponse вы должны предоставить информацию о конкретном типе в момент вызова метода readEntity. Использование специализированного GenericType позволяет избежать ситуации, когда данные десериализуются в LinkedHashMap, и обеспечивает правильное создание объектов вашего обобщенного типа. Это решение улучшит стабильность и предсказуемость ваших тестов, что жизненно важно для эффективного API.

Оцените материал
Добавить комментарий

Капча загружается...