3

I'm writing a Rest API and my automated tests are calling the class directly without deploying the to the server. As an example, I am testing this method:

@GET
@Path("/{referenceId}")
@Produces("application/json")
public String findByReferenceId(@PathParam("referenceId") String referenceId,
                                String view) {

My tests are checking that the logic works and they pass. But this code has a bug: I forgot to put a @QueryParam annotation on that view parameter. So this code works when tested, but if you try to use this resource on the deployed app, the view parameter will never be settable.

There are many ways I can solve this, but my current preference is to somehow write an automated check that if a method has a @Path annotation, then every parameter must have either a @PathParam, a @QueryParam or whatever other valid annotation can be there.

I prefer this over a new end-to-end test, because my other tests are already covering 95% of that logic. I just don't know how to automate this check. I'm using Maven and CXF (which means I'm using Spring). I'm hoping there's a plugin that can be configured to do this.


Something I just realized: It's valid to have a single parameter without an annotation. When you do this, jax-rs sets it to the entity you pass in. I'm not sure how to deal with this scenario. I could create my own custom annotation called @Payload and tell people to use it, but something seems wrong about that.

Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • Uhm, good question... You can do that through reflection but then would that really be a test? Meh, maybe – fge Apr 15 '15 at 21:54
  • @fge I'd be ok with that I think. I'm using an XML Spring configuration. I'm not sure how to grab all the Service classes, yet. – Daniel Kaplan Apr 15 '15 at 21:58
  • What exactly do annotations do? We have just started using them in my class via a CollegeBoard AP project. Whoever wrote the project claims that `@Override` annotation produces an error if you mistype a method header you are overriding. Say you were overriding `toString()`,but your header was typed `tostring()`. The explanation made it sound like the immediately preceding `@Override` annotation would notice this mistake and return an error. Is this true? – Ungeheuer Apr 15 '15 at 22:07
  • Well, I don't know Spring, but libraries do exist which can scan the classpath for you and find classes meeting certain criteria... From the on reflection can be used to inspect method arguments. I don't know how far you've been in that direction, but you can obtain a list of `Parameter`s from a `Method` and you can query the annotations of these `Parameter`s. – fge Apr 15 '15 at 22:08
  • 1
    @JohnnyCoder I suggest you join [the Java chat room](http://chat.stackoverflow.com/rooms/139/java) or search a little more for this question. Annotations are pretty well documented but elements on them are "sparse" (you need to read a lot to get a global picture) – fge Apr 15 '15 at 22:09
  • 1
    @johnnycoder The Override annotion is a compile time annotion which marks you intention to override the specified method from its para rent class. If the compiler is unable resolve the relationship, it fails the compilation, which means you can catch typos at compile time, rather then needing to debug the code – MadProgrammer Apr 15 '15 at 22:11
  • @fge thank you for that extremely quick response. I'm going to hit up that link, but when you say elements, what exactly do you mean? – Ungeheuer Apr 15 '15 at 22:12
  • @JohnnyCoder that there is more to annotations than just `@Override`; think retention policy, targetting, etc etc – fge Apr 15 '15 at 22:13
  • @MadProgrammer Really?!?! That seems insanely useful and convenient. Does it just say that you have problem, or does it return the line of the annotation that it is throwing the error? – Ungeheuer Apr 15 '15 at 22:13
  • Y'all are extremely informative. I appreciate this help a lot and I apologize @DanielKaplan for invading your question with a sub-question. :) – Ungeheuer Apr 15 '15 at 22:14
  • @JohnnyCoder again, join the chat room linked above; a full explanation can be given. But comments on an existing question _are not_ the place for that – fge Apr 15 '15 at 22:14

1 Answers1

1

Here's my solution. In the end, I decided to create a @RawPayload annotation. Otherwise, I can't know if the missing annotation is intentional or not. Here's where I got the Reflections class: https://code.google.com/p/reflections/

import org.junit.Test;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;

import javax.ws.rs.Path;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Set;

import static org.junit.Assert.assertTrue;

...

@Test
public void testAllParametersAreAnnotated() throws Exception {
    String message = "You are missing a jax-rs annotation on a method's parameter: ";
    Reflections reflections = new Reflections("package.for.my.services", new MethodAnnotationsScanner());
    Set<Method> resourceMethods = reflections.getMethodsAnnotatedWith(Path.class);
    assertTrue(resourceMethods.size() > 0);

    for (Method resourceMethod : resourceMethods) {
        for (int i = 0; i < resourceMethod.getGenericParameterTypes().length; i++) {
            Annotation[] annotations = resourceMethod.getParameterAnnotations()[i];
            boolean annotationExists = annotations.length > 0;
            assertTrue(message +
                            resourceMethod.getDeclaringClass().getCanonicalName() +
                            "#" +
                            resourceMethod.getName(),
                    annotationExists && containsJaxRsAnnotation(annotations));
        }
    }
}

private boolean containsJaxRsAnnotation(Annotation[] annotations) {
    for (Annotation annotation : annotations) {
        if (annotation instanceof RawPayload) {
            return true;
        }
        if (annotation.annotationType().getCanonicalName().startsWith("javax.ws.rs")) {
            return true;
        }
    }
    return false;
}

Here's my annotation:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * I'm creating this marker so that we can put it on raw payload params.  This is normally unnecessary,
 * but it lets me write a very useful automated test.
 */
@Retention(RetentionPolicy.RUNTIME)
public @interface RawPayload {
}
Daniel Kaplan
  • 62,768
  • 50
  • 234
  • 356
  • Well, that was what I had in mind but you can greatly improve this test if you use assertj and its `SoftAssertions` – fge Apr 15 '15 at 23:17
  • @fge that's a pretty cool tool. But, this test is practically instantaneous, so I don't mind rerunning it. – Daniel Kaplan Apr 15 '15 at 23:21
  • Well, what I mean is this; say you have a method with two parameters and both are missing annotations. What will happen is that the test will fail because the first parameter is not annotated, you then add the annotation to the first parameter; on the second run, the test will fail _again_. Soft assertions allow you to have a report about missing annotations for both parameters at once. Hence my suggestion. – fge Apr 15 '15 at 23:28