惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Full Disclosure
WordPress大学
WordPress大学
小众软件
小众软件
Cloudbric
Cloudbric
AWS News Blog
AWS News Blog
腾讯CDC
量子位
人人都是产品经理
人人都是产品经理
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Vulnerabilities – Threatpost
Scott Helme
Scott Helme
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Hacker News
The Hacker News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
Jina AI
Jina AI
Attack and Defense Labs
Attack and Defense Labs
S
SegmentFault 最新的问题
Simon Willison's Weblog
Simon Willison's Weblog
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
T
Tailwind CSS Blog
Last Week in AI
Last Week in AI
博客园 - 【当耐特】
Google Online Security Blog
Google Online Security Blog
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
罗磊的独立博客
L
LINUX DO - 最新话题
博客园 - Franky
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
The Last Watchdog
The Last Watchdog
J
Java Code Geeks
AI
AI
C
Cisco Blogs
酷 壳 – CoolShell
酷 壳 – CoolShell
C
Cyber Attacks, Cyber Crime and Cyber Security
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
雷峰网
雷峰网
Help Net Security
Help Net Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
云风的 BLOG
云风的 BLOG
I
Intezer
S
Securelist

Maciej Walkowiak - Java & Spring

Blog Generating HTTP clients in Spring Boot application from OpenAPI spec PostgreSQL and UUID as primary key Dynamic Projections with Spring Data JPA Container logs with Spring Boot and Testcontainers Reified Generics in Java? Faster integration tests with reusable Testcontainers and Flyway Running one-time jobs with Quartz and Spring Boot The best way to use Testcontainers with Spring Boot Spring Boot & Flyway - clear database between integration tests What's new in Spring? Activate Maven Profile by Operating System Spring Boot with Thymeleaf and Tailwind CSS - Complete Guide How to publish a Java library to Maven Central - Complete Guide Docker Compose - waiting until containers are ready Single file Java applications with JBang Beautiful bash scripts with Gum Running Java on CRaC How to log PostgreSQL queries with Testcontainers Spring Boot 3.0 & GraalVM Native Image - not a free lunch Creating Spring Cloud Function projects with AWS SAM Creating Project Templates with Cookiecutter Auto-Registering JUnit 5 extensions Spring Boot component scanning without annotations Listing Maven dependencies in Spring Boot Actuator Info endpoint Spring Cloud AWS 2.3 RC2 Released How I built vlad-cli - command line interface to Vlad Mihalcea The State of Java Relational Persistence On Choosing a Tech Stack
Loading classpath resources to String with a custom JUnit extension
2022-07-30 · via Maciej Walkowiak - Java & Spring
Published on
  • Spring Boot
  • Junit

Quite frequently when writing integration test I have to use a String usually containing JSON representing either an HTTP request, or a payload of a message for example, RabbitMQ.

Till Java 13 (or in practice till Java 17), multiline strings in Java were pain to maintain, because Java did not really support multiline strings!

java

String payload= "{\n"
        +"    \"resource\": \"/{proxy+}\",\n"
        +"    \"path\": \"/path/to/resource\",\n"
        +"    \"httpMethod\": \"POST\",\n"
        +"    \"isBase64Encoded\": false,\n"
        +"    \"queryStringParameters\": {\n"
        +"      \"foo\": \"bar\"\n"
        +"    }\n"
        +"  }\n";

Things got better with Java 13 and text blocks:

java

String json = """
        {
            "resource": "/{proxy+}",
            "path": "/path/to/resource",
            "httpMethod": "POST",
            "isBase64Encoded": false,
            "queryStringParameters": {
              "foo": "bar"
            }
          }
        """;

So now, if the payload is small, and does make the test unreadable, I tend just to use it directly in the Java code, but when the payload is larger or I am on Java 11/8, I prefer to put these JSON structures in src/test/resources and just load them in test methods.

Spring comes with a resource abstraction which lets us load resources from classpath, file system, URLs and much more in a clean declarative way:

java

@Value("classpath:payload.json");
Resource payload;

This also works on the JUnit test methods:

java

@Test
void someTest(@Value("classpath:payload.json") Resource payload) {
    // ...
}

The problem is though - I need a String, not a Resource, and the Resource does not expose a method that returns the content as a String. So I always end up either looking at my old code or Googling for how to convert an InputStream to String end end up with a method like:

java

@SpringBootTest
class AppTests {

    @Test
    void someTest(@Value("classpath:payload.json") Resource payload) {
        String content = asString(payload);
    }

    static String asString(Resource resource) {
        try (InputStream is = resource.getInputStream()) {
            return StreamUtils.copyToString(is, UTF_8);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

Not too bad but I would like putting an annotation to be enough. What I need is:

java

@SpringBootTest
class AppTests {
    @Test
    void contextLoads(@StringResource("classpath:payload.json") String payload) {
        // ...
    }
}

Spring based @StringResource

@StringResource of course does not exist, so lets create it and lets make a JUnit 5 extension - specifically a parameter resolver - that will load this classpath resource to String.

The annotation code is fairly straightforward:

java

import org.junit.jupiter.api.extension.ExtendWith;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StringResource {
    String value();
}

Now the parameter resolver. It is much simpler than I initially thought, because we can access application context created with @SpringBootTest using SpringExtension.getApplicationContext(..) method:

java

public class StringResourceParameterResolver implements ParameterResolver {
    @Override 
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException {
        return parameterContext.isAnnotated(StringResource.class) && parameterContext.getParameter().getType()
                .equals(String.class);
    }

    @Override 
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException {
        ApplicationContext applicationContext = SpringExtension.getApplicationContext(extensionContext);
        Resource resource = applicationContext.getResource(
                parameterContext.findAnnotation(StringResource.class).map(StringResource::value).orElseThrow());
        return asString(resource);
    }

    private static String asString(Resource resource) {
        try (InputStream is = resource.getInputStream()) {
            return StreamUtils.copyToString(is, UTF_8);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

To use an extension, we need to enable it. There are several ways how extensions can be enabled, with putting @ExtendWith(StringResourceParameterResolver.class) as the most straightforward one. But, we can follow the same approach that is used in JUnit 5 own @ParameterizedTest annotation. We can put @ExtendWith on the StringResource annotation itself!

java

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ExtendWith(StringResourceParameterResolver.class)
public @interface StringResource {
    String value();
}

No-Spring @StringResource alternative ​

The above approach works great in Spring integration tests, but of course won't work for regular unit tests or in general non-spring tests.

Spring independent version of StringResourceParameterResolver looks like this:

java

public class StringResourceParameterResolver implements ParameterResolver {
    @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException {
        return parameterContext.isAnnotated(StringResource.class) && parameterContext.getParameter().getType()
                .equals(String.class);
    }

    @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException {
        String location = parameterContext.findAnnotation(StringResource.class).map(StringResource::value).orElseThrow();

        try (InputStream is = getClass().getClassLoader().getResourceAsStream(location)) {
            return new String(is.readAllBytes(), UTF_8);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }
}

But you need to keep in mind that since it only resolves files from classpath, it does not support any prefixes so you use it as following:

java

@Test
void someTest(@StringResource("foo.txt") String foo) throws IOException {
    // ..
}

Conclusion ​

This is just an example of an elegant solution to an annoying problem and I hope it perhaps may inspire you to create custom JUnit extensions that helps you get rid of boilerplate in your project.

Let's stay in touch and follow me on Twitter: @maciejwalkowiak

Subscribe to RSS feed