diff --git a/core/src/main/java/org/microshed/testing/jupiter/MicroShedTestExtension.java b/core/src/main/java/org/microshed/testing/jupiter/MicroShedTestExtension.java index 333a9351..677bf30c 100644 --- a/core/src/main/java/org/microshed/testing/jupiter/MicroShedTestExtension.java +++ b/core/src/main/java/org/microshed/testing/jupiter/MicroShedTestExtension.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019 IBM Corporation and others + * Copyright (c) 2019,2020 IBM Corporation and others * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -21,6 +21,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -44,15 +45,16 @@ */ class MicroShedTestExtension implements BeforeAllCallback { - static final Logger LOGGER = LoggerFactory.getLogger(MicroShedTestExtension.class); + static final Logger LOG = LoggerFactory.getLogger(MicroShedTestExtension.class); @Override public void beforeAll(ExtensionContext context) throws Exception { Class testClass = context.getRequiredTestClass(); ApplicationEnvironment config = ApplicationEnvironment.load(); - LOGGER.info("Using ApplicationEnvironment class: " + config.getClass().getCanonicalName()); + LOG.info("Using ApplicationEnvironment class: " + config.getClass().getCanonicalName()); config.applyConfiguration(testClass); config.start(); + configureRestAssured(config); injectRestClients(testClass); } @@ -79,7 +81,7 @@ private static void injectRestClients(Class clazz) { Object restClient = rcBuilder.build(restClientField.getType()); try { restClientField.set(null, restClient); - LOGGER.debug("Injected rest client for " + restClientField); + LOG.debug("Injected rest client for " + restClientField); } catch (Exception e) { throw new ExtensionConfigurationException("Unable to set field " + restClientField, e); } @@ -99,14 +101,60 @@ private static String createJwtIfNeeded(Field restClientField) { return null; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void configureRestAssured(ApplicationEnvironment config) { + Class RestAssured = tryLoad("io.restassured.RestAssured"); + if (RestAssured == null) + return; + + try { + URL appURL = new URL(config.getApplicationURL()); + String baseURI = appURL.getProtocol() + "://" + appURL.getHost(); + int port = appURL.getPort(); + String basePath = appURL.getPath(); + LOG.info("Configuring RestAssured with baseURI=" + baseURI + " port=" + port + " basePath=" + basePath); + + RestAssured.getField("baseURI").set(null, baseURI); + RestAssured.getField("basePath").set(null, basePath); + RestAssured.getField("port").set(null, port); + } catch (Exception e) { + LOG.warn("Unable to configure REST Assured because of: " + e.getMessage(), e); + } + + try { + // Configure JSONB as the JSON object mapper by invoking: + // ObjectMapperType JSONB = ObjectMapperType.JSONB; + // ObjectMapperConfig omConfig = ObjectMapperConfig.objectMapperConfig().defaultObjectMapperType(JSONB); + // RestAssured.config = RestAssured.config.objectMapperConfig(omConfig); + ClassLoader cl = MicroShedTestExtension.class.getClassLoader(); + Class ObjectMapperType = (Class) Class.forName("io.restassured.mapper.ObjectMapperType", false, cl); + Object JSONB = Enum.valueOf(ObjectMapperType, "JSONB"); + Class ObjectMapperConfig = Class.forName("io.restassured.config.ObjectMapperConfig", false, cl); + Object omConfig = ObjectMapperConfig.getMethod("objectMapperConfig").invoke(null); + omConfig = omConfig.getClass().getMethod("defaultObjectMapperType", ObjectMapperType).invoke(omConfig, JSONB); + Class RestAssuredConfig = Class.forName("io.restassured.config.RestAssuredConfig", false, cl); + Object raConfig = RestAssured.getField("config").get(null); + raConfig = RestAssuredConfig.getMethod("objectMapperConfig", ObjectMapperConfig).invoke(raConfig, omConfig); + RestAssured.getField("config").set(null, raConfig); + LOG.debug("Regsitered JSONB ObjectMapper for REST Assured"); + } catch (IllegalArgumentException e) { + // Prior to RestAssured 4.2.0 the ObjectMapperType.JSONB enum is not available + LOG.debug("Unable to configure JSON-B object mapper for REST Assured", e); + } catch (Exception e) { + LOG.warn("Unable to configure JSON-B object mapper for REST Assured", e); + } + } + @SuppressWarnings("unchecked") private static Optional> getMpRestClient() { + return Optional.ofNullable((Class) tryLoad("org.eclipse.microprofile.rest.client.inject.RestClient")); + } + + private static Class tryLoad(String clazz) { try { - return Optional.of((Class) Class.forName("org.eclipse.microprofile.rest.client.inject.RestClient", - false, - MicroShedTestExtension.class.getClassLoader())); + return Class.forName(clazz, false, MicroShedTestExtension.class.getClassLoader()); } catch (ClassNotFoundException | LinkageError e) { - return Optional.empty(); + return null; } } } diff --git a/docs/features/14_RestAssured.md b/docs/features/14_RestAssured.md new file mode 100644 index 00000000..24c056d2 --- /dev/null +++ b/docs/features/14_RestAssured.md @@ -0,0 +1,106 @@ +--- +layout: post +title: "REST Assured" +--- + +MicroShed Testing provides auto-configuration for when [REST Assured](https://github.com/rest-assured/rest-assured) is available on the test classpath. REST Assured is a Java DSL library for easy testing of REST services. It is more verbose than using a REST client, but offers more direct control over the request and response.f + +## Enable REST Assured + +To enable REST Assured, add the following dependency to your pom.xml: + +```xml + + io.rest-assured + rest-assured + 4.2.0 + test + +``` + +Any version of REST Assured will have basic integration with MicroShed Testing -- the application URL and port will be auto-configured. + +As of REST Assured 4.2.0 or newer, a JSON-B based JSON ObjectMapper will be auto-configured. + +Because of the auto-configuration, no specific configuration is required in your test classes. + +## Example usage + +Validating a simple `GET` request: + +```java +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +// ... + +@MicroShedTest +public class RestAssuredTest { + + @Container + public static ApplicationContainer app = new ApplicationContainer() + .withAppContextRoot("/myservice"); + + @Test + public void testCreatePerson() { + given() + .queryParam("name", "Hank") + .queryParam("age", 45) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON); + } +} +``` + +It is also possible to send/receive POJOs with the JSON-B based ObjectMapper: + +```java + @Test + public void testGetPerson() { + // First create the Person + long bobId = given() + .queryParam("name", "Bob") + .queryParam("age", 24) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(long.class); + + // Validate new created Person can be retrieved + Person bob = given() + .pathParam("personId", bobId) + .when() + .get("/{personId}") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(Person.class); + assertEquals("Bob", bob.name); + assertEquals(24, bob.age); + assertNotNull(bob.id); + } +``` + +For a complete working example, see the [RestAssuredTest class](https://github.com/MicroShed/microshed-testing/blob/master/sample-apps/everything-app/src/test/java/org/example/app/RestAssuredTest.java) + +## Auto-configuration override + +If you would like to use a different JSON ObjectMapper besides the default (JSON-B/Yasson), you can run the following code in your test initialization flow: + +```java +import io.restassured.RestAssured; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.mapper.ObjectMapperType; +// ... + +ObjectMapperConfig omConfig = ObjectMapperConfig.objectMapperConfig().defaultObjectMapperType(ObjectMapperType.JACKSON_2); +RestAssured.config = RestAssured.config.objectMapperConfig(omConfig); +``` diff --git a/docs/features/98_Examples.md b/docs/features/98_Examples.md index d7cf24ab..100c7d85 100644 --- a/docs/features/98_Examples.md +++ b/docs/features/98_Examples.md @@ -9,6 +9,7 @@ Sometimes code is worth a thousand words. Here are some pointers to working exam - [Basic JAX-RS application using Gradle](https://github.com/MicroShed/microshed-testing/tree/master/sample-apps/jaxrs-json) - [Basic JAX-RS application using Maven](https://github.com/MicroShed/microshed-testing/tree/master/sample-apps/maven-app) +- [Basic JAX-RS application using REST Assured](https://github.com/MicroShed/microshed-testing/blob/master/sample-apps/everything-app/src/test/java/org/example/app/RestAssuredTest.java) - [JAX-RS and JDBC applicaiton using a PostgreSQL database](https://github.com/MicroShed/microshed-testing/tree/master/sample-apps/jdbc-app) - [JAX-RS application secured with MP JWT](https://github.com/MicroShed/microshed-testing/tree/master/sample-apps/jaxrs-mpjwt) - [JAX-RS and MongoDB application that depends on an external REST service](https://github.com/MicroShed/microshed-testing/tree/master/sample-apps/everything-app) diff --git a/sample-apps/everything-app/build.gradle b/sample-apps/everything-app/build.gradle index db6cacd1..6e006448 100644 --- a/sample-apps/everything-app/build.gradle +++ b/sample-apps/everything-app/build.gradle @@ -10,6 +10,7 @@ dependencies { testCompile group: 'org.slf4j', name: 'slf4j-log4j12', version: '1.7.29' testCompile 'org.testcontainers:mockserver:1.12.3' testCompile 'org.mock-server:mockserver-client-java:5.5.4' + testCompile 'io.rest-assured:rest-assured:4.2.0' testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2' } diff --git a/sample-apps/everything-app/src/test/java/org/example/app/RestAssuredTest.java b/sample-apps/everything-app/src/test/java/org/example/app/RestAssuredTest.java new file mode 100644 index 00000000..8440b341 --- /dev/null +++ b/sample-apps/everything-app/src/test/java/org/example/app/RestAssuredTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2020 IBM Corporation and others + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.example.app; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.microshed.testing.SharedContainerConfig; +import org.microshed.testing.jupiter.MicroShedTest; + +@MicroShedTest +@SharedContainerConfig(AppContainerConfig.class) +public class RestAssuredTest { + + @Test + public void testCreatePerson() { + // Verify POST /?name=Hank&age=45 returns HTTP 200 + given() + .queryParam("name", "Hank") + .queryParam("age", 45) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON); + } + + @Test + public void testMinSizeName() { + // First create a new person with min size name + long minSizeNameId = given() + .queryParam("name", "Bob") + .queryParam("age", 42) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(long.class); + + // Verify they exist after creation + Person p = given() + .pathParam("personId", minSizeNameId) + .when() + .get("/{personId}") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(Person.class); + assertEquals("Bob", p.name); + assertEquals(42, p.age); + assertEquals(minSizeNameId, p.id); + } + + @Test + public void testMinAge() { + long minAgeId = given() + .queryParam("name", "Newborn") + .queryParam("age", 0) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(long.class); + + Person p = given() + .pathParam("personId", minAgeId) + .when() + .get("/{personId}") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(Person.class); + assertEquals("Newborn", p.name); + assertEquals(0, p.age); + assertEquals(minAgeId, p.id); + } + + @Test + public void testGetPerson() { + // First create the Person + long bobId = given() + .queryParam("name", "Bob") + .queryParam("age", 24) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(long.class); + + // Validate new created Person can be retrieved + Person bob = given() + .pathParam("personId", bobId) + .when() + .get("/{personId}") + .then() + .statusCode(200) + .contentType(JSON) + .extract() + .as(Person.class); + assertEquals("Bob", bob.name); + assertEquals(24, bob.age); + assertNotNull(bob.id); + } + + @Test + public void testGetUnknownPerson() { + given() + .pathParam("personId", -1L) + .when() + .get("/{personId}") + .then() + .statusCode(404); + } + + @Test + public void testCreateBadPersonNullName() { + given() + .queryParam("name", (Object[]) null) + .queryParam("age", 5) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(400); + } + + @Test + public void testCreateBadPersonNegativeAge() { + given() + .queryParam("name", "NegativeAgePersoN") + .queryParam("age", -1) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(400); + } + + @Test + public void testCreateBadPersonNameTooLong() { + given() + .queryParam("name", "NameTooLongPersonNameTooLongPersonNameTooLongPerson") + .queryParam("age", 5) + .contentType(JSON) + .when() + .post("/") + .then() + .statusCode(400); + } + +}