Integration test for single Spring Boot #Service class - java

I'm writing an integration test for this Spring Boot #Service bean
import org.springframework.stereotype.Service;
import org.thymeleaf.ITemplateEngine;
import org.thymeleaf.context.Context;
import java.util.Locale;
import java.util.Map;
#Service
public class ThymeLeafEmailTemplateService implements EmailTemplateService {
private final ITemplateEngine springTemplateEngine;
public ThymeLeafEmailTemplateService(ITemplateEngine springTemplateEngine) {
this.springTemplateEngine = springTemplateEngine;
}
public String generateEmailBody(String template, Map<String, Object> variables) {
Context context = new Context(Locale.getDefault(), variables);
return springTemplateEngine.process(template, context);
}
}
Currently, the test class is defined as shown below
#SpringBootTest
class ThymeLeafEmailTemplateServiceTests {
#Autowired
private EmailTemplateService service;
#Test
void generateTaskNotificationEmail() {
var output = service.generateEmailBody("/template", Map.of());
assertEquals("Expected Output", output);
}
}
A problem with this approach is that it's very slow/inefficient because the entire application context is loaded, but I really only need the service being tested and its dependencies.
If I change the test class' annotations to
#SpringBootTest
#ContextConfiguration(classes = ThymeLeafEmailTemplateService.class)
the test fails, because the dependency ITemplateEngine springTemplateEngine does not exist. I could add this dependency to classes (the list of beans to create), but this seems like a very brittle approach.
Is there an efficient way to integration test a single #Service?
Note:
I know I could mock ITemplateEngine springTemplateEngine and write a unit test instead, but I want to test the template's actual output, so this approach won't work

You can use
#WebMvcTest(ThymeLeafEmailTemplateService.class)
This will load only that bean in your application context along with any default Spring configuration beans.

Related

Testing custom JsonDeserializer in Jackson / SpringBoot

I am trying to write a unit test to a custom deserializer that is instantiated using a constructor with an #Autowired parameter and my entity marked with #JsonDeserialize. It works fine in my integration tests where a MockMvc brings up spring serverside.
However with tests where objectMapper.readValue(...) is being called, a new instance of deserializer using default constructor with no parameters is instantiated. Even though
#Bean
public MyDeserializer deserializer(ExternalObject externalObject)
instantiates wired version of deserializer, real call is still passed to empty constructor and context is not being filled up.
I tried manually instantiating of a deserializer instance and registering it in ObjectMapper, but it only works if I remove #JsonDeserialize from my entity class (and it breaks my integration tests even if I do the same in my #Configuration class.) - looks related to this: https://github.com/FasterXML/jackson-databind/issues/1300
I can still test the deserializer behavior calling deserializer.deserialize(...) directly, but this approach doesn't work for me in tests that are not Deserializer's unit tests...
UPD: working code below
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.github.tomakehurst.wiremock.common.Json;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import java.io.IOException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
#JsonTest
#RunWith(SpringRunner.class)
public class JacksonInjectExample {
private static final String JSON = "{\"field1\":\"value1\", \"field2\":123}";
public static class ExternalObject {
#Override
public String toString() {
return "MyExternalObject";
}
}
#JsonDeserialize(using = MyDeserializer.class)
public static class MyEntity {
public String field1;
public String field2;
public String name;
public MyEntity(ExternalObject eo) {
name = eo.toString();
}
#Override
public String toString() {
return name;
}
}
#Component
public static class MyDeserializer extends JsonDeserializer<MyEntity> {
#Autowired
private ExternalObject external;
public MyDeserializer() {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
}
public MyDeserializer(#JacksonInject final ExternalObject external) {
this.external = external;
}
#Override
public MyEntity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
return new MyEntity(external);
}
}
#Configuration
public static class TestConfiguration {
#Bean
public ExternalObject externalObject() {
return new ExternalObject();
}
#Bean
public MyDeserializer deserializer(ExternalObject externalObject) {
return new MyDeserializer(externalObject);
}
}
#Test
public void main() throws IOException {
HandlerInstantiator hi = mock(HandlerInstantiator.class);
MyDeserializer deserializer = new MyDeserializer();
deserializer.external = new ExternalObject();
doReturn(deserializer).when(hi).deserializerInstance(any(), any(), eq(MyDeserializer.class));
final ObjectMapper mapper = Json.getObjectMapper();
mapper.setHandlerInstantiator(hi);
final MyEntity entity = mapper.readValue(JSON, MyEntity.class);
Assert.assertEquals("MyExternalObject", entity.name);
}
}
I don't know how to set this particularly using Jackson injection, but you can test it using spring Json tests. I think this method is closer to the real scenario and much more simplier. Spring will load only related to serialization/deserialization beans, thus you have to provide only custom beans or mocks instead them.
#JsonTest
public class JacksonInjectExample {
private static final String JSON = "{\"field1\":\"value1\", \"field2\":123}";
#Autowired
private JacksonTester<MyEntity> jacksonTester;
#Configuration
public static class TestConfiguration {
#Bean
public ExternalObject externalObject() {
return new ExternalObject();
}
}
#Test
public void test() throws IOException {
MyEntity result = jacksonTester.parseObject(JSON);
assertThat(result.getName()).isEqualTo("MyExternalObject");
}
If you would like to use mocks use following snippet:
#MockBean
private ExternalObject externalObject;
#Test
public void test() throws IOException {
when(externalObject.toString()).thenReturn("Any string");
MyEntity result = jacksonTester.parseObject(JSON);
assertThat(result.getName()).isEqualTo("Any string");
}
Very interesting question, it made me wonder how autowiring into jackson deserializers actually works in a spring application. The jackson facility that is used seems to be the HandlerInstantiator interface, which is configured by spring to the SpringHandlerInstantiator implementation, which just looks up the class in the application context.
So in theory you could setup an ObjectMapper in your unit test with your own (mocked) HandlerInstantiator, returning a prepared instance from deserializerInstance(). It seems to be fine to return null for other methods or when the class parameter does not match, this will cause jackson to create the instance on its own.
However, I do not think this is a good way to unit test deserialization logic, as the ObjectMapper setup is necessarily different from what is used during actual application execution. Using the JsonTest annotation as suggested in Anton's answer would be a much better approach, as you are getting the same json configuration that would be used during runtime.
Unit tests should not depend upon or invoke other major classes or frameworks. This is especially true if there are also integration or acceptance tests covering the functioning of the application with a particular set of dependencies as you describe. So it would be best to write the unit test so that it has a single class as its subject i.e. calling deserializer.deserialize(...) directly.
In this case a unit test should consist of instanciating a MyDeserializer with a mocked or stubbed ExternalObject, then testing that its deserialize() method returns a MyEntity correctly for different states of the JsonParser and DeserializationContext arguments. Mockito is really good for setting up mock dependencies!
By using an ObjectMapper in the unit test, quite a lot of code from the Jackson framework is also being invoked in each run - so the test is not verifying the contract of MyDeserializer, it is verifying the behaviour of the combination of MyDeserializer and a particular release of Jackson. If there is a failure of the test it won't be immediatly clear which of all the components involved is at fault. And because setting up the environment of the two frameworks together is more difficult the test will prove brittle over time and fail more often due to issues with the setup in the test class.
The Jackson framework is responsible for writing unit tests of ObjectMapper.readValue and constructors using #JacksonInject. For the 'other unit tests that are not Deserializer's unit tests' - it would be best to mock/stub the MyDeserializer (or other dependencies) for that test. That way the other class's logic is being isolated from the logic in MyDeserializer - and the other class's contracts can be verified without being qualified by the behaviour of code outside of the unit under test.

Spring Boot Integration Testing with mocked Services/Components

Situation and Problem: In Spring Boot, how can I inject one or more mocked classes/beans into the application to do an integration test? There are a few answers on StackOverflow, but they are focused on the situation before Spring Boot 1.4 or are just not working for me.
The background is, that in the code bellow the implementation of Settings relies on third party servers and other external systems. The functionality of Settings is already tested in a unit test, so for a full integration test I want to mock out the dependency to these servers or system and just provide dummy values.
MockBean will ignore all existing bean definitions and provide a dummy object, but this object doesn't provide a method behavior in other classes that inject this class. Using the #Before way to set the behavior before a test doesn't influence the injected object or isn't injected in other application services like AuthenticationService.
My question: How can I inject my beans into the application context?
My test:
package ch.swaechter.testapp;
import ch.swaechter.testapp.utils.settings.Settings;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.junit4.SpringRunner;
#TestConfiguration
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {MyApplication.class})
public class MyApplicationTests {
#MockBean
private Settings settings;
#Before
public void before() {
Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");
}
#Test
public void contextLoads() {
String applicationsecret = settings.getApplicationSecret();
System.out.println("Application secret: " + applicationsecret);
}
}
And bellow a service that should use the mocked class, but doesn't receive this mocked class:
package ch.swaechter.testapp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
#Service
public class AuthenticationServiceImpl implements AuthenticationService {
private final Settings settings;
#Autowired
public AuthenticationServiceImpl(Settings settings) {
this.settings = settings;
}
#Override
public boolean loginUser(String token) {
// Use the application secret to check the token signature
// But here settings.getApplicationSecret() will return null (Instead of Application Secret as specified in the mock)!
return false;
}
}
Looks like you are using Settings object before you specify its mocked behavior.
You have to run
Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");
during configuration setup. For preventing that you can create special configuration class for test only.
#RunWith(SpringRunner.class)
#SpringBootTest(classes = {MyApplication.class, MyApplicationTest.TestConfig.class})
public class MyApplicationTest {
private static final String SECRET = "Application Secret";
#TestConfiguration
public static class TestConfig {
#Bean
#Primary
public Settings settingsBean(){
Settings settings = Mockito.mock(Settings.class);
Mockito.when(settings.getApplicationSecret()).thenReturn(SECRET);
Mockito.doReturn(SECRET).when(settings).getApplicationSecret();
return settings;
}
}
.....
}
Also I would recommend you to use next notation for mocking:
Mockito.doReturn(SECRET).when(settings).getApplicationSecret();
It will not run settings::getApplicationSecret
When you annotate a field with #MockBean, spring will create a mock of the annotated class and use it to autowire all beans of the application context.
You must not create the mock yourself with
Settings settings = Mockito.mock(Settings.class);
this would create a second mock, leading to the described problem.
Solution :
#MockBean
private Settings settings;
#Before
public void before() {
Mockito.when(settings.getApplicationSecret()).thenReturn("Application Secret");
}
#Test
public void contextLoads() {
String applicationsecret = settings.getApplicationSecret();
System.out.println("Application secret: " + applicationsecret);
}

SpringBoot unit test configuration

I created a spring-boot 1.4.0 application and I would like to internationlize it using yaml file.
I created a class for loading the configuration from the yaml file like it is explained in the documentation here http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties.
I would like to create a test to check that my class has correctly loaded the properties from the yaml file.
If we keep the exemple from the documentation how to create a unit test that will load a yaml file (with a different name that application.yml) and check that the method getUsername() will return the value from the yaml file ?
Here is the code I have but still can't load the username :
#Component
#ConfigurationProperties(locations = "classpath:mylocalizedprops.yml", prefix="connection")
public class ConnectionProperties {
private String username;
// ... getters and setters
}
and the test class
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(classes = Application.class)
public class InternationalizationTest {
#Autowired
private ConnectionProperties connectionProperties;
public void propsShouldBeNotNull() {
assertNotNull(connectionProperties);
}
public void userNameShouldBeCorrect() {
assertEquals(connectionProperties.getUsername(), expectedUserName);
}
}
I have failed the userNameShouldBeCorrect test. The file mylocalizedprops.yml is located in the src/main/resources folder of a Maven structured application.
I would consider this an integration test, not a unit-test because you are testing the interaction between various components. Regardless, here is how I would do it.
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = YourApplication.class)
public class InternationalizationTests() {
#Autowired
ConnectionProperties connectionProperties;
#Test
public void testCorrectTranslationLoaded() {
Assert.assertEquals("english-username", connectionProperties.getUsername());
}
}
You can also create a test configuration if you would like to, which you can specify which translation to load. You would then need different classes to test different configurations. See the docs: http://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html
Unit test can be done easily with Jmockit
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import mockit.Mock;
import mockit.MockUp;
import mockit.Mocked;
import mockit.Verifications;
class RuleApiApplicationTest {
#Mocked
private ConfigurableApplicationContext mockedContext;
#Test
void testApplicationRun() {
new MockUp<SpringApplication>() {
#Mock
public ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return mockedContext;
}
};
RuleApiApplication.main(new String[]{});
new Verifications() {{
SpringApplication.run(RuleApiApplication.class, new String[]{});
}};
}
}

Spring's #Retryable not working when running JUnit Test

I have this test:
#RunWith(MockitoJUnitRunner.class)
public class myServiceTest {
#InjectMocks
myService subject;
private myService spy;
#Before
public void before() {
spy = spy(subject);
}
#Test
public void testing() {
when(spy.print2()).thenThrow(new RuntimeException()).thenThrow(new RuntimeException()).thenReturn("completed");
spy.print1();
verify(spy, times(3)).print2();
}
and then I have:
#Service("myService")
public class myService extends myAbstractServiceClass {
public String print1() {
String temp = "";
temp = print2();
return temp;
}
#Retryable
public String print2() {
return "completed";
}
}
then I have this interface(which my abstractService implements):
public interface myServiceInterface {
#Retryable(maxAttempts = 3)
String print1() throws RuntimeException;
#Retryable(maxAttempts = 3)
String print2() throws RuntimeException;
}
but, I get a runtimeexception thrown when I run the test, leading me to believe it is not retrying. Am I doing this wrong?
This is because you are not using the SpringJUnitClassRunner.
Mockito and your own classes are not taking the #Retryable annotation in account. So you rely on the implementation of Spring to do so. But your test does not activate Spring.
This is from the SpringJUnit4ClassRunner JavaDoc:
SpringJUnit4ClassRunner is a custom extension of JUnit's BlockJUnit4ClassRunner which provides functionality of the Spring TestContext Framework to standard JUnit tests by means of the TestContextManager and associated support classes and annotations.
To use this class, simply annotate a JUnit 4 based test class with #RunWith(SpringJUnit4ClassRunner.class) or #RunWith(SpringRunner.class).
You should restructure your test class at least to something like:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes=MyConfig.class)
public class MyServiceTest {
#Configuration
#EnableRetry
#Import(myService.class)
public static class MyConfig {}
...
What am I doing there?
activate the Spring JUnit hook
specify the Spring context configuration class
define the spring configuration and import your service as a bean
enable the retryable annotation
Are there some other pitfalls?
Yes, you are using Mockito to simulate an exception. If you want to test this behaviour with Spring like this, you should have a look at Springockito Annotations.
But be aware of that: Springockito you will replace the spring bean completely which forces you to proxy the call of your retryable. You need a structure like: test -> retryableService -> exceptionThrowingBean. Then you can use Springockito or what ever you like e.g. ReflectionTestUtils to configure the exceptionThrowingBean with the behaviour you like.
You should reference the interface type of your service in your test: MyServiceInterface
And last but not least. There is a naming convention nearly all Java developers follow: class names have first letter of each internal word capitalized
Hope that helps.
Another way:
#EnableRetry
#RunWith(SpringRunner.class)
#SpringBootTest(classes={ServiceToTest.class})
public class RetryableTest {
#Autowired
private ServiceToTest serviceToTest;
#MockBean
private ComponentInsideTestClass componentInsideTestClass;
#Test
public void retryableTest(){
serviceToTest.method();
}
}
I think you should let Spring manage the bean, create the appropriate proxy and handle the process.
If you want to mock specific beans, you can create mocks and inject them to the service under test.
1st option could be unwrapping proxied service, creating mocks and manually injecting them:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {RetryConfiguration.class})
#DirtiesContext
public class TheServiceImplTest {
#Autowired
private TheService theService;
#Before
public void setUp(){
TheService serviceWithoutProxy = AopTestUtils.getUltimateTargetObject(theService);
RetryProperties mockRetryProperties = Mockito.mock(RetryProperties.class);
ReflectionTestUtils.setField(serviceWithoutProxy, "retryProperties", mockRetryProperties);
}
#Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
}
In this example, I mocked one bean, RetryProperties, and injected into the service. Also note that, in this approach you are modifying the test application context which is cached by Spring. This means that if you don't use #DirtiesContext, service will continue its way with mocked bean in other tests. You can read more here
Second option would be creating a test specific #Configuration and mock the depended bean there. Spring will pick up this new mocked bean instead of the original one:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {RetryConfiguration.class, TheServiceImplSecondTest.TestConfiguration.class})
public class TheServiceImplSecondTest {
#Autowired
private TheService theService;
#Test
public void shouldFetch() {
Assert.assertNotNull(theService);
}
#Configuration
static class TestConfiguration {
#Bean
public RetryProperties retryProperties() {
return Mockito.mock(RetryProperties.class);
}
}
}
In this example, we have defined a test specific configuration and added it to the #ContextConfiguration.

Make ApplicationContext dirty before and after test class

I have a particular class (let's say MyTest) in my Spring integration tests that is using PowerMock #PrepareForTest annotation on a Spring component: #PrepareForTest(MyComponent.class). This means that PowerMock will load this class with some modifications. The problem is, my #ContextConfiguration is defined on the superclass which is extended by MyTest, and the ApplicationContext is cached between different test classes. Now, if MyTest is run first, it will have the correct PowerMock version of MyComponent, but if not - the test will fail since the context will be loaded for another test (without #PrepareForTest).
So what I want to do is to reload my context before MyTest. I can do that via
#DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
But what if I also want to reload context after this test is done? So I will have clean MyComponent again without PowerMock modifications. Is there a way to do both BEFORE_CLASS and AFTER_CLASS?
For now I did it with the following hack:
#FixMethodOrder(MethodSorters.NAME_ASCENDING)
on MyTest and then
/**
* Stub test to reload ApplicationContext before execution of real test methods of this class.
*/
#DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD)
#Test
public void aa() {
}
/**
* Stub test to reload ApplicationContext after execution of real test methods of this class.
*/
#DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
#Test
public void zz() {
}
I am wondering if there is a prettier way to do that?
As a side question, is it possible to reload only certain bean and not full context?
Is there a way to do both BEFORE_CLASS and AFTER_CLASS?
No, that is unfortunately not supported via #DirtiesContext.
However, what you're really saying is that you want a new ApplicationContext for MyTest that is identical to the context for the parent test class but only lives as long as MyTest. And... you don't want to affect the context cached for the parent test class.
So with that in mind, the following trick should do the job.
#RunWith(SpringJUnit4ClassRunner.class)
// Inherit config from parent and combine with local
// static Config class to create a new context
#ContextConfiguration
#DirtiesContext
public class MyTest extends BaseTests {
#Configuration
static class Config {
// No need to define any actual #Bean methods.
// We only need to add an additional #Configuration
// class so that we get a new ApplicationContext.
}
}
Alternative to #DirtiesContext
If you want to have a context dirtied both before and after a test class, you can implement a custom TestExecutionListener that does exactly that. For example, the following will do the trick.
import org.springframework.core.Ordered;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;
public class DirtyContextBeforeAndAfterClassTestExecutionListener
extends AbstractTestExecutionListener {
#Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
#Override
public void beforeTestClass(TestContext testContext) throws Exception {
testContext.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
}
#Override
public void afterTestClass(TestContext testContext) throws Exception {
testContext.markApplicationContextDirty(HierarchyMode.EXHAUSTIVE);
}
}
You can then use the custom listener in MyTest as follows.
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestExecutionListeners.MergeMode;
#TestExecutionListeners(
listeners = DirtyContextBeforeAndAfterClassTestExecutionListener.class,
mergeMode = MergeMode.MERGE_WITH_DEFAULTS
)
public class MyTest extends BaseTest { /* ... */ }
As a side question, is it possible to reload only certain bean and not full context?
No, that is also not possible.
Regards,
Sam (author of the Spring TestContext Framework)

Categories