Browse Source

[Proposal] generate mocked clients from feign interfaces (#1092)

* First attempt at test stub code generation

* Using templating framework

* Cleanup dependencies

* Annotate generated classes as experimental
pull/1106/head
Marvin Froeder 5 years ago committed by GitHub
parent
commit
74b9a73f3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 66
      apt-test-generator/README.md
  2. 185
      apt-test-generator/pom.xml
  3. 27
      apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java
  4. 29
      apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java
  5. 158
      apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java
  6. 40
      apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java
  7. 62
      apt-test-generator/src/main/resources/stub.mustache
  8. 98
      apt-test-generator/src/test/java/example/github/GitHubStub.java
  9. 48
      apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java
  10. 8
      example-github/src/main/java/example/github/GitHubExample.java
  11. 1
      pom.xml

66
apt-test-generator/README.md

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
# Feign APT test generator
This module generates mock clients for tests based on feign interfaces
## Usage
Just need to add this module to dependency list and Java [Annotation Processing Tool](https://docs.oracle.com/javase/7/docs/technotes/guides/apt/GettingStarted.html) will automatically pick up the jar and generate test clients.
There are 2 main alternatives to include this to a project:
1. Just add to classpath and java compiler should automaticaly detect and run code generation. On maven this is done like this:
```xml
<dependency>
<groupId>io.github.openfeign.experimental</groupId>
<artifactId>feign-apt-test-generator</artifactId>
<version>${feign.version}</version>
<scope>test</scope>
</dependency>
```
1. Use a purpose build tool that allow to pick output location and don't mix dependencies onto classpath
```xml
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-test-sources/feign</outputDirectory>
<processor>feign.apttestgenerator.GenerateTestStubAPT</processor>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>io.github.openfeign.experimental</groupId>
<artifactId>feign-apt-test-generator</artifactId>
<version>${feign.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>feign-stubs-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>target/generated-test-sources/feign</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
```

185
apt-test-generator/pom.xml

@ -0,0 +1,185 @@ @@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2012-2019 The Feign Authors
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.github.openfeign</groupId>
<artifactId>parent</artifactId>
<version>10.5.2-SNAPSHOT</version>
</parent>
<groupId>io.github.openfeign.experimental</groupId>
<artifactId>feign-apt-test-generator</artifactId>
<name>Feign APT test generator</name>
<description>Feign code generation tool for mocked clients</description>
<properties>
<main.basedir>${project.basedir}/..</main.basedir>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-example-github</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
<version>0.18</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc5</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<targetPath>docker</targetPath>
<filtering>true</filtering>
<!-- Replace maven properties in the docker file so we can get artifacts etc -->
<directory>${project.basedir}/docker</directory>
</resource>
<!-- need to manually specify the resources to copy because we have a manual setting above -->
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>feign.aptgenerator.github.GitHubFactoryExample</mainClass>
</transformer>
</transformers>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.skife.maven</groupId>
<artifactId>really-executable-jar-maven-plugin</artifactId>
<version>1.5.0</version>
<configuration>
<programFile>github</programFile>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>really-executable-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<!-- used to create docker images -->
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<!-- docker file copied here after maven replaces properties -->
<dockerDirectory>${project.build.directory}/classes/docker/</dockerDirectory>
<!-- Pull image before build, otherwise end up with image not found if it was never downloaded before -->
<pullOnBuild>true</pullOnBuild>
<serverId>docker-hub</serverId>
<registryUrl>https://index.docker.io/v1/</registryUrl>
<imageName>feign-apt-generator/test</imageName>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.artifactId}-${project.version}.jar</include>
</resource>
</resources>
</configuration>
<executions>
<!-- see definition of how this runs above -->
<execution>
<phase>post-integration-test</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

27
apt-test-generator/src/main/java/feign/apttestgenerator/ArgumentDefinition.java

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 feign.apttestgenerator;
public class ArgumentDefinition {
public final String name;
public final String type;
public ArgumentDefinition(String name, String type) {
super();
this.name = name;
this.type = type;
}
}

29
apt-test-generator/src/main/java/feign/apttestgenerator/ClientDefinition.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 feign.apttestgenerator;
public class ClientDefinition {
public final String jpackage;
public final String className;
public final String fullQualifiedName;
public ClientDefinition(String jpackage, String className, String fullQualifiedName) {
super();
this.jpackage = jpackage;
this.className = className;
this.fullQualifiedName = fullQualifiedName;
}
}

158
apt-test-generator/src/main/java/feign/apttestgenerator/GenerateTestStubAPT.java

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 feign.apttestgenerator;
import com.github.jknack.handlebars.*;
import com.github.jknack.handlebars.context.FieldValueResolver;
import com.github.jknack.handlebars.context.JavaBeanValueResolver;
import com.github.jknack.handlebars.context.MapValueResolver;
import com.github.jknack.handlebars.io.URLTemplateSource;
import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableList;
import java.io.IOError;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import javax.annotation.processing.*;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.WildcardType;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;
@SupportedAnnotationTypes({
"feign.RequestLine"
})
@AutoService(Processor.class)
public class GenerateTestStubAPT extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println(annotations);
System.out.println(roundEnv);
final Map<TypeElement, List<ExecutableElement>> clientsToGenerate = annotations.stream()
.map(roundEnv::getElementsAnnotatedWith)
.flatMap(Set::stream)
.map(ExecutableElement.class::cast)
.collect(Collectors.toMap(
annotatedMethod -> TypeElement.class.cast(annotatedMethod.getEnclosingElement()),
ImmutableList::of,
(list1, list2) -> ImmutableList.<ExecutableElement>builder()
.addAll(list1)
.addAll(list2)
.build()));
System.out.println("Count: " + clientsToGenerate.size());
System.out.println("clientsToGenerate: " + clientsToGenerate);
final Handlebars handlebars = new Handlebars();
final URLTemplateSource source =
new URLTemplateSource("stub.mustache", getClass().getResource("/stub.mustache"));
Template template;
try {
template = handlebars.with(EscapingStrategy.JS).compile(source);
} catch (final IOException e) {
throw new IOError(e);
}
clientsToGenerate.forEach((type, executables) -> {
try {
final String jPackage = readPackage(type);
final String className = type.getSimpleName().toString();
final JavaFileObject builderFile = processingEnv.getFiler()
.createSourceFile(jPackage + "." + className + "Stub");
final ClientDefinition client = new ClientDefinition(
jPackage,
className,
type.toString());
final List<MethodDefinition> methods = executables.stream()
.map(method -> {
final String methodName = method.getSimpleName().toString();
final List<ArgumentDefinition> args = method.getParameters()
.stream()
.map(var -> new ArgumentDefinition(var.getSimpleName().toString(),
var.asType().toString()))
.collect(Collectors.toList());
return new MethodDefinition(
methodName,
method.getReturnType().toString(),
method.getReturnType().getKind() == TypeKind.VOID,
args);
})
.collect(Collectors.toList());
final Context context = Context.newBuilder(template)
.combine("client", client)
.combine("methods", methods)
.resolver(JavaBeanValueResolver.INSTANCE, MapValueResolver.INSTANCE,
FieldValueResolver.INSTANCE)
.build();
final String stubSource = template.apply(context);
System.out.println(stubSource);
builderFile.openWriter().append(stubSource).close();
} catch (final Exception e) {
e.printStackTrace();
processingEnv.getMessager().printMessage(Kind.ERROR,
"Unable to generate factory for " + type);
}
});
return true;
}
private Type toJavaType(TypeMirror type) {
outType(type.getClass());
if (type instanceof WildcardType) {
}
return Object.class;
}
private void outType(Class<?> class1) {
if (Object.class.equals(class1) || class1 == null) {
return;
}
System.out.println(class1);
outType(class1.getSuperclass());
Arrays.stream(class1.getInterfaces()).forEach(this::outType);
}
private String readPackage(Element type) {
if (type.getKind() == ElementKind.PACKAGE) {
return type.toString();
}
if (type.getKind() == ElementKind.CLASS
|| type.getKind() == ElementKind.INTERFACE) {
return readPackage(type.getEnclosingElement());
}
return null;
}
}

40
apt-test-generator/src/main/java/feign/apttestgenerator/MethodDefinition.java

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 feign.apttestgenerator;
import com.google.common.base.CaseFormat;
import com.google.common.base.Converter;
import java.util.List;
public class MethodDefinition {
private static final Converter<String, String> TO_UPPER_CASE =
CaseFormat.LOWER_CAMEL.converterTo(CaseFormat.UPPER_CAMEL);
private final String name;
private final String uname;
private final String returnType;
private final boolean isVoid;
private final List<ArgumentDefinition> args;
public MethodDefinition(String name, String returnType, boolean isVoid,
List<ArgumentDefinition> args) {
super();
this.name = name;
this.uname = TO_UPPER_CASE.convert(name);
this.returnType = returnType;
this.isVoid = isVoid;
this.args = args;
}
}

62
apt-test-generator/src/main/resources/stub.mustache

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
package {{client.jpackage}};
import java.util.concurrent.atomic.AtomicInteger;
import feign.Experimental;
public class {{client.className}}Stub
implements {{client.fullQualifiedName}} {
@Experimental
public class {{client.className}}Invokations {
{{#each methods as |method|}}
private final AtomicInteger {{method.name}} = new AtomicInteger(0);
public int {{method.name}}() {
return {{method.name}}.get();
}
{{/each}}
}
@Experimental
public class {{client.className}}Anwsers {
{{#each methods as |method|}}
{{#unless method.isVoid}}
private {{method.returnType}} {{method.name}}Default;
{{/unless}}
{{/each}}
}
public {{client.className}}Invokations invokations;
public {{client.className}}Anwsers answers;
public {{client.className}}Stub() {
this.invokations = new {{client.className}}Invokations();
this.answers = new {{client.className}}Anwsers();
}
{{#each methods as |method|}}
{{#unless method.isVoid}}
@Experimental
public {{client.className}}Stub with{{method.uname}}({{method.returnType}} {{method.name}}) {
answers.{{method.name}}Default = {{method.name}};
return this;
}
{{/unless}}
@Override
public {{method.returnType}} {{method.name}}({{#each method.args as |arg|}}{{arg.type}} {{arg.name}}{{#unless @last}},{{/unless}}{{/each}}) {
invokations.{{method.name}}.incrementAndGet();
{{#unless method.isVoid}}
return answers.{{method.name}}Default;
{{/unless}}
}
{{/each}}
}

98
apt-test-generator/src/test/java/example/github/GitHubStub.java

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 example.github;
import java.util.concurrent.atomic.AtomicInteger;
import feign.Experimental;
public class GitHubStub
implements example.github.GitHubExample.GitHub {
@Experimental
public class GitHubInvokations {
private final AtomicInteger repos = new AtomicInteger(0);
public int repos() {
return repos.get();
}
private final AtomicInteger contributors = new AtomicInteger(0);
public int contributors() {
return contributors.get();
}
private final AtomicInteger createIssue = new AtomicInteger(0);
public int createIssue() {
return createIssue.get();
}
}
@Experimental
public class GitHubAnwsers {
private java.util.List<example.github.GitHubExample.GitHub.Repository> reposDefault;
private java.util.List<example.github.GitHubExample.GitHub.Contributor> contributorsDefault;
}
public GitHubInvokations invokations;
public GitHubAnwsers answers;
public GitHubStub() {
this.invokations = new GitHubInvokations();
this.answers = new GitHubAnwsers();
}
@Experimental
public GitHubStub withRepos(java.util.List<example.github.GitHubExample.GitHub.Repository> repos) {
answers.reposDefault = repos;
return this;
}
@Override
public java.util.List<example.github.GitHubExample.GitHub.Repository> repos(java.lang.String owner) {
invokations.repos.incrementAndGet();
return answers.reposDefault;
}
@Experimental
public GitHubStub withContributors(java.util.List<example.github.GitHubExample.GitHub.Contributor> contributors) {
answers.contributorsDefault = contributors;
return this;
}
@Override
public java.util.List<example.github.GitHubExample.GitHub.Contributor> contributors(java.lang.String owner,
java.lang.String repo) {
invokations.contributors.incrementAndGet();
return answers.contributorsDefault;
}
@Override
public void createIssue(example.github.GitHubExample.GitHub.Issue issue,
java.lang.String owner,
java.lang.String repo) {
invokations.createIssue.incrementAndGet();
}
}

48
apt-test-generator/src/test/java/feign/apttestgenerator/GenerateTestStubAPTTest.java

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
/**
* Copyright 2012-2019 The Feign Authors
*
* 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 feign.apttestgenerator;
import static com.google.testing.compile.CompilationSubject.assertThat;
import static com.google.testing.compile.Compiler.javac;
import com.google.testing.compile.Compilation;
import com.google.testing.compile.JavaFileObjects;
import org.junit.Test;
import java.io.File;
/**
* Test for {@link GenerateTestStubAPT}
*/
public class GenerateTestStubAPTTest {
private final File main = new File("../example-github/src/main/java/").getAbsoluteFile();
@Test
public void test() throws Exception {
final Compilation compilation =
javac()
.withProcessors(new GenerateTestStubAPT())
.compile(JavaFileObjects.forResource(
new File(main, "example/github/GitHubExample.java")
.toURI()
.toURL()));
assertThat(compilation).succeeded();
assertThat(compilation)
.generatedSourceFile("example.github.GitHubStub")
.hasSourceEquivalentTo(JavaFileObjects.forResource(
new File("src/test/java/example/github/GitHubStub.java")
.toURI()
.toURL()));
}
}

8
example-github/src/main/java/example/github/GitHubExample.java

@ -30,17 +30,17 @@ public class GitHubExample { @@ -30,17 +30,17 @@ public class GitHubExample {
private static final String GITHUB_TOKEN = "GITHUB_TOKEN";
interface GitHub {
public interface GitHub {
class Repository {
public class Repository {
String name;
}
class Contributor {
public class Contributor {
String login;
}
class Issue {
public class Issue {
Issue() {

1
pom.xml

@ -47,6 +47,7 @@ @@ -47,6 +47,7 @@
<module>example-github</module>
<module>example-wikipedia</module>
<module>mock</module>
<module>apt-test-generator</module>
<module>benchmark</module>
</modules>

Loading…
Cancel
Save