-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16574 from geoand/allow-mocking-singletons
Introduce the ability to mock @singleton beans
- Loading branch information
Showing
11 changed files
with
289 additions
and
1 deletion.
There are no files selected for viewing
11 changes: 11 additions & 0 deletions
11
...on-tests/injectmock/src/main/java/io/quarkus/it/mockbean/CapitalizerServiceSingleton.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
import javax.inject.Singleton; | ||
|
||
@Singleton | ||
public class CapitalizerServiceSingleton { | ||
|
||
public String capitalize(String input) { | ||
return input.toUpperCase(); | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
...tion-tests/injectmock/src/main/java/io/quarkus/it/mockbean/GreetingResourceSingleton.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
import javax.ws.rs.GET; | ||
import javax.ws.rs.Path; | ||
import javax.ws.rs.Produces; | ||
|
||
@Path("greetingSingleton") | ||
public class GreetingResourceSingleton { | ||
|
||
final MessageServiceSingleton messageService; | ||
final SuffixServiceSingleton suffixService; | ||
final CapitalizerServiceSingleton capitalizerService; | ||
|
||
public GreetingResourceSingleton(MessageServiceSingleton messageService, SuffixServiceSingleton suffixService, | ||
CapitalizerServiceSingleton capitalizerService) { | ||
this.messageService = messageService; | ||
this.suffixService = suffixService; | ||
this.capitalizerService = capitalizerService; | ||
} | ||
|
||
@GET | ||
@Produces("text/plain") | ||
public String greet() { | ||
return capitalizerService.capitalize(messageService.getMessage() + suffixService.getSuffix()); | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
...ration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/MessageServiceSingleton.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
public interface MessageServiceSingleton { | ||
|
||
String getMessage(); | ||
} |
12 changes: 12 additions & 0 deletions
12
...on-tests/injectmock/src/main/java/io/quarkus/it/mockbean/MessageServiceSingletonImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
import javax.inject.Singleton; | ||
|
||
@Singleton | ||
public class MessageServiceSingletonImpl implements MessageServiceSingleton { | ||
|
||
@Override | ||
public String getMessage() { | ||
return "hello"; | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
...gration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/SuffixServiceSingleton.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
public class SuffixServiceSingleton { | ||
|
||
String getSuffix() { | ||
return ""; | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
...tests/injectmock/src/main/java/io/quarkus/it/mockbean/SuffixServiceSingletonProducer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
import javax.enterprise.inject.Produces; | ||
import javax.inject.Singleton; | ||
|
||
public class SuffixServiceSingletonProducer { | ||
|
||
@Produces | ||
@Singleton | ||
public SuffixServiceSingleton dummyService() { | ||
return new SuffixServiceSingleton(); | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
...-tests/injectmock/src/test/java/io/quarkus/it/mockbean/GreetingSingletonResourceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package io.quarkus.it.mockbean; | ||
|
||
import static io.restassured.RestAssured.given; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.mockito.ArgumentMatchers.anyString; | ||
|
||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
import org.mockito.invocation.InvocationOnMock; | ||
import org.mockito.stubbing.Answer; | ||
|
||
import io.quarkus.test.junit.QuarkusTest; | ||
import io.quarkus.test.junit.mockito.InjectMock; | ||
|
||
@QuarkusTest | ||
class GreetingSingletonResourceTest { | ||
|
||
@InjectMock(convertScopes = true) | ||
MessageServiceSingleton messageService; | ||
|
||
@InjectMock(convertScopes = true) | ||
SuffixServiceSingleton suffixService; | ||
|
||
@InjectMock(convertScopes = true) | ||
CapitalizerServiceSingleton capitalizerService; | ||
|
||
@Test | ||
public void testGreet() { | ||
Mockito.when(messageService.getMessage()).thenReturn("hi"); | ||
Mockito.when(suffixService.getSuffix()).thenReturn("!"); | ||
mockCapitalizerService(); | ||
|
||
given() | ||
.when().get("/greetingSingleton") | ||
.then() | ||
.statusCode(200) | ||
.body(is("hi!")); | ||
} | ||
|
||
@Test | ||
public void testGreetAgain() { | ||
Mockito.when(messageService.getMessage()).thenReturn("yolo"); | ||
Mockito.when(suffixService.getSuffix()).thenReturn("!!!"); | ||
mockCapitalizerService(); | ||
|
||
given() | ||
.when().get("/greetingSingleton") | ||
.then() | ||
.statusCode(200) | ||
.body(is("yolo!!!")); | ||
} | ||
|
||
@Test | ||
public void testMocksNotSet() { | ||
// when mocks are not configured, they return the Mockito default response | ||
Assertions.assertNull(messageService.getMessage()); | ||
Assertions.assertNull(suffixService.getSuffix()); | ||
|
||
given() | ||
.when().get("/greetingSingleton") | ||
.then() | ||
.statusCode(204); | ||
} | ||
|
||
private void mockCapitalizerService() { | ||
Mockito.doAnswer(new Answer() { // don't upper case the string, leave it as it is | ||
@Override | ||
public Object answer(InvocationOnMock invocationOnMock) throws Throwable { | ||
return invocationOnMock.getArgument(0); | ||
} | ||
}).when(capitalizerService).capitalize(anyString()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
.../junit/mockito/internal/SingletonToApplicationScopedTestBuildChainCustomizerProducer.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package io.quarkus.test.junit.mockito.internal; | ||
|
||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Set; | ||
import java.util.function.Consumer; | ||
import java.util.function.Predicate; | ||
|
||
import org.jboss.jandex.AnnotationInstance; | ||
import org.jboss.jandex.AnnotationTarget; | ||
import org.jboss.jandex.AnnotationValue; | ||
import org.jboss.jandex.ClassInfo; | ||
import org.jboss.jandex.DotName; | ||
import org.jboss.jandex.Index; | ||
import org.jboss.jandex.MethodInfo; | ||
|
||
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; | ||
import io.quarkus.arc.processor.AnnotationsTransformer; | ||
import io.quarkus.arc.processor.DotNames; | ||
import io.quarkus.builder.BuildChainBuilder; | ||
import io.quarkus.builder.BuildContext; | ||
import io.quarkus.builder.BuildStep; | ||
import io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer; | ||
import io.quarkus.test.junit.mockito.InjectMock; | ||
|
||
public class SingletonToApplicationScopedTestBuildChainCustomizerProducer implements TestBuildChainCustomizerProducer { | ||
|
||
private static final DotName INJECT_MOCK = DotName.createSimple(InjectMock.class.getName()); | ||
|
||
@Override | ||
public Consumer<BuildChainBuilder> produce(Index testClassesIndex) { | ||
return new Consumer<BuildChainBuilder>() { | ||
|
||
@Override | ||
public void accept(BuildChainBuilder buildChainBuilder) { | ||
buildChainBuilder.addBuildStep(new BuildStep() { | ||
@Override | ||
public void execute(BuildContext context) { | ||
Set<DotName> mockTypes = new HashSet<>(); | ||
List<AnnotationInstance> instances = testClassesIndex.getAnnotations(INJECT_MOCK); | ||
for (AnnotationInstance instance : instances) { | ||
if (instance.target().kind() != AnnotationTarget.Kind.FIELD) { | ||
continue; | ||
} | ||
AnnotationValue allowScopeConversionValue = instance.value("convertScopes"); | ||
if ((allowScopeConversionValue != null) && allowScopeConversionValue.asBoolean()) { | ||
// we need to fetch the type of the bean, so we need to look at the type of the field | ||
mockTypes.add(instance.target().asField().type().name()); | ||
} | ||
} | ||
if (mockTypes.isEmpty()) { | ||
return; | ||
} | ||
|
||
// TODO: this annotation transformer is too simplistic and should be replaced | ||
// by whatever build item comes out of the implementation | ||
// of https://github.com/quarkusio/quarkus/issues/16572 | ||
context.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { | ||
@Override | ||
public boolean appliesTo(AnnotationTarget.Kind kind) { | ||
return (kind == AnnotationTarget.Kind.CLASS) || (kind == AnnotationTarget.Kind.METHOD); | ||
} | ||
|
||
@Override | ||
public void transform(TransformationContext transformationContext) { | ||
AnnotationTarget target = transformationContext.getTarget(); | ||
if (target.kind() == AnnotationTarget.Kind.CLASS) { // scope on bean case | ||
ClassInfo classInfo = target.asClass(); | ||
if (isMatchingBean(classInfo)) { | ||
if (classInfo.classAnnotation(DotNames.SINGLETON) != null) { | ||
replaceSingletonWithApplicationScoped(transformationContext); | ||
} | ||
} | ||
} else if (target.kind() == AnnotationTarget.Kind.METHOD) { // CDI producer case | ||
MethodInfo methodInfo = target.asMethod(); | ||
if ((methodInfo.annotation(DotNames.PRODUCES) != null) | ||
&& (methodInfo.annotation(DotNames.SINGLETON) != null)) { | ||
DotName returnType = methodInfo.returnType().name(); | ||
if (mockTypes.contains(returnType)) { | ||
replaceSingletonWithApplicationScoped(transformationContext); | ||
} | ||
} | ||
} | ||
} | ||
|
||
private void replaceSingletonWithApplicationScoped(TransformationContext transformationContext) { | ||
transformationContext.transform().remove(new IsSingletonPredicate()) | ||
.add(DotNames.APPLICATION_SCOPED).done(); | ||
} | ||
|
||
// this is very simplistic and is the main reason why the annotation transformer strategy | ||
// is fine with most cases, but it can't cover all cases | ||
private boolean isMatchingBean(ClassInfo classInfo) { | ||
// class type matches | ||
if (mockTypes.contains(classInfo.name())) { | ||
return true; | ||
} | ||
if (mockTypes.contains(classInfo.superName())) { | ||
return true; | ||
} | ||
for (DotName iface : classInfo.interfaceNames()) { | ||
if (mockTypes.contains(iface)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
})); | ||
} | ||
}).produces(AnnotationsTransformerBuildItem.class).build(); | ||
} | ||
}; | ||
} | ||
|
||
private static class IsSingletonPredicate implements Predicate<AnnotationInstance> { | ||
@Override | ||
public boolean test(AnnotationInstance annotationInstance) { | ||
return annotationInstance.name().equals(DotNames.SINGLETON); | ||
} | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
...urces/META-INF/services/io.quarkus.test.junit.buildchain.TestBuildChainCustomizerProducer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
io.quarkus.test.junit.mockito.internal.UnremoveableMockTestBuildChainCustomizerProducer | ||
io.quarkus.test.junit.mockito.internal.SingletonToApplicationScopedTestBuildChainCustomizerProducer |