How to avoid a second Instantiation of a spring bean in child test context - spring

I created an Embedded Sftp server bean for my integration tests, i hooked the startup and the shutdown of the server respectively with the afterPropertiesSet and destroy life cycles
public class EmbeddedSftpServer implements InitializingBean, DisposableBean {
//other class content
#Override
public void afterPropertiesSet() throws Exception {
//Code for starting server here
}
#Override
public void destroy() throws Exception {
//Code for stopping server here
}
}
here my config class
#TestConfiguration
public class SftpTestConfig {
#Bean
public EmbeddedSftpServer embeddedSftpServer() {
return new EmbeddedSftpServer();
}
//Other bean definitions
}
Now when i inject the bean in my test classes like the following :
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleOneIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleTwoIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
#SpringBatchTest
#ContextConfiguration(classes = SftpTestConfig .class)
class ExampleThreeIT {
#Autowired
private EmbeddedSftpServer embeddedSftpServer;
}
And i run all the test classes simultaneously, i found out that for the test classes annotated with #ExtendWith(SpringExtension.class), it's the same context that is used (which is understandable since i guess spring cache it) and therefore the bean lifecycle methods are not executed again, but to my surprise, for the class annotated with #SpringBatchTest i noticed that the life cycle hooks of the bean are executed again! Which is a behavior that is not convenient since i want the application context to start the server one time for all tests and close it at the end of those tests (which is the case if i use only #ExtendWith(SpringExtension.class) for all my test classes).
N.B. : I need to use #SpringBachTest for my ExampleThreeIT test class.

I think you are hitting this issue: https://github.com/spring-projects/spring-batch/issues/3940 which has been fixed in v4.3.4/4.2.8. Upgrading to one of these versions should fix your issue.

Related

Add one additional bean to "#WebMvcTest"

I have a controller and a test using #WebMvcTest and its running fine. Now i needed to add a little validation logic and for this i #Autowired an additional bean (a #Component, a MapstructMapper).
As expected now the test is failing due to #WebMvcTest. (No components are discovered)
Is there a way to add one bean to the context created?
Since i am using #MockBeans to mock service layer: is there a way to delegate all mock calls to a real object? With this i could mock the mapper and delegate to real mapper?!
A simple way of getting additional beans in the context is via using nested configuration classes within test classes
#TestConfiguration
static class AdditionalConfig {
#Bean
public SomeBean getSomeBean() {
return new SomeBean());
}
}
Example:
Scenario - If you have some Controller say ProductController and you have the corresponding slice-test for the class say ProductionControllerTest
#RestController
public class ProductController {
#Autowired
private IProductService productService;
#Autowired
private IProductValidator productValidator;
#GetMapping("product")
public Product getProduct(#RequestParam Long id) {
Product product = productService.getProduct(id); // you are using mockBean of productService
productValidator.validateProduct(product); // you need real bean of productValidator
return product;
}
}
Corresponding slide test class with an additional bean configuration
#RunWith(SpringRunner.class)
#WebMvcTest
public class ProductControllerSliceTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private IProductService productService;
#Autowired
private ApplicationContext applicationContext;
#TestConfiguration
static class AdditionalConfig {
#Bean
public IProductValidator productValidator() {
return new ProductValidator();
}
}
#Test
public void testProductGetById() throws Exception {
Product testProductWithID1L = new Product(1L, "testProduct");
when(productService.getProduct(anyLong())).thenReturn(testProductWithID1L);
mockMvc.perform(get("/product")
.param("id", "1")).andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("name")
.value("testProduct"))
.andExpect(MockMvcResultMatchers.jsonPath("id")
.value("1"));
}
}
Additional thoughts on your scenario: If you really intend to do the unit testing of the controller class then ideally you should mock all the additional dependencies of class that you are testing.
Ideally, the intention of the unit test is to only test the behavior of the object/class under test. All the dependent classes behavior or external calls should be mocked.
When you start testing several classes together under one test, you are moving more towards a component test or integration test
A very simple solution is to annotate your test class with #Import specifying the class(es) of the additional bean(s) you want to use in your test, as stated in documentation:
Typically #WebMvcTest is used in combination with #MockBean or #Import
to create any collaborators required by your #Controller beans.
e.g.
#WebMvcTest(MyController.class)
#Import(SomeOtherBean.class)
public class SourcingOrganisationControllerTests {
// The controller bean is made available via #WebMvcTest
#Autowired
private MyController myController;
// Additional beans (only one in this case) are made available via #Import
#Autowired
private SomeOtherBean someOtherBean;
}

Is there a way to include a spring component in a WebMvcTest

Given production code classes:
#RestController
#RequiredArgsConstructor
public class MyController {
private final MyValidator validator;
// annotations relating to request mapping excluded for brevity
public void test(#Valid #RequestBody final MyParams params) {
// do stuff
}
#InitBinder
#SuppressWarnings("unused")
protected void initBinder(final WebDataBinder binder) {
binder.setValidator(validator);
}
}
and
#Component
#RequiredArgsConstructor
public class MyValidator implements Validator {
...
#Override
public void validate(final Object target, final Errors errors) {
// custom validation
}
}
and finally test code:
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
// tests
}
I encounter the error:
NoSuchBeanDefinitionException: No qualifying bean of type 'MyValidator' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
I think the error is fair enough. I've annotated the test as a WebMvcTest, which I believe has excluded #Component beans. This is intentional and desired (from the perspective that I am only wanting to test the "web layer", not the whole context - it just so happens I need a component which is related/used only in the controllers)
My question, therefore, is: how can one explicitly include a component like a validator in the test context for a web test?
My environment is java version "10.0.2" 2018-07-17, spring boot 1.5.16.RELEASE.
There are two ways to solve this.
Using #SpringBootTest and #AutoConfigureMvc instead of #RunWith(SpringRunner.class) and #WebMvcTest.
#SpringBootTest
#AutoConfigureMvc
public class MyControllerTest {
}
Creating a #TestConfiguration class that injects the 'MyValidator' bean as:
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#TestConfiguration
static class TestConfig {
#Bean
MyValidator getMyValidator(){
return new MyValidator();
}
}
// tests
}
More on this can be found here : https://mkyong.com/spring-boot/spring-boot-how-to-init-a-bean-for-testing/
There are two ways to test the web layer
first.
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyControllerTest {
#Autowired
private MyController myController;
}
The #SpringBootTest annotation tells Spring Boot to go and look for a
main configuration class (one with #SpringBootApplication for
instance), and use that to start a Spring application context.
A nice feature of the Spring Test support is that the application
context is cached in between tests, so if you have multiple methods in
a test case, or multiple test cases with the same configuration, they
only incur the cost of starting the application once. You can control
the cache using the #DirtiesContext annotation.
Secondly, if you want to use the #WebMvcTest(MyController.class)
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#MockBean
private MyValidator validator;
}
But this validator is a fake, so you have to customize it for testing.
See this link for more details https://spring.io/guides/gs/testing-web/
I cannot recommend it as a standard practice but if you do need an instance of a dependency in your Web MVC tests (for example in legacy code) you can add them into the spring context using #SpyBean annotation.
Real methods of that class will be called during the test and you can verify them if needed similarly to the beans annotated with #MockBean
#RunWith(SpringRunner.class)
#WebMvcTest(MyController.class)
public class MyControllerTest {
#SpyBean
private MyValidator validator
}

Spring HATEOAS Resource assembler is not instantiated in unit test

I am trying to write a unit test for a REST controller which generates HATEOAS links via Resource assembler class. Everything is OK in production, but with the unit test Resource assembler class is not being injected into the controller.
my resource assembler class is:
#Component
public class ModelResourceAssembler extends ResourceAssemblerSupport<Model, ModelResource> {
public ModelResourceAssembler() {
super(ModelRestController.class, ModelResource.class);
}
#Bean
public ModelResourceAssembler modelResourceAssembler(){
return new ModelResourceAssembler();
}
#Override
public ModelResource toResource(Model model) {
...
}
}
The controller is:
#Controller
#RequestMapping("/demo")
#ComponentScan(basePackages = {"com.foo.demo"} )
public class ModelRestController {
#Autowired
private ModelPersistenceHandler modelPersistenceHandler;
#Autowired
private ModelResourceAssembler modelResourceAssembler;
...
}
And the unit test:
#RunWith(MockitoJUnitRunner.class)
#ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes= {ModelResourceAssembler.class, ModelRestController.class})
public class ModelRestControllerTest {
private MockMvc mockMvc;
#InjectMocks
private ModelRestController modelRestController;
#Mock
private ModelPersistenceHandler modelPersistenceHandler;
#Before
public void setup() {
MockitoAnnotations.initMocks(this);
mockMvc = MockMvcBuilders.standaloneSetup(modelRestController).build();
}
...
}
No matter what I do the ModelResourceAssembler instance is always null. Since the application is Spring Boot it does not have the WebCoonfig classes and autowired WebApplicationContext is always null, so I cannot (and really don't want to since I am running a unit test) instantiate MockMvc via webAppContextSetup
The solution ended up being quite simple: I needed to add one line to my test:
#Spy
private ModelResourceAssembler modelResourceAssembler;
And the bean was instantiated and properly wired
In your example you use #InjectMocks but don't declare a mock for ModelResourceAssembler. You don't get an instance out of nowhere.
You use the MockitoJUnitRunner.class. It has no idea of Spring beans. For testing Spring applications you rather want to use SpringJUnit4ClassRunner.class.
If i may suggest, if you use constructor injection for your controller then you can just mock the dependency and not need spring junit test runner stuff.

#Autowired bean is null in Test Listener class

This question was asked before Using Autowired in a TestExecutionListener class for BeforeClass junit however it wasn't answered. I am facing the same problem but haven't figured out the solution
Example: I am getting null mapper.
public class CustomExecutionListener extends AbstractTestExecutionListener {
#Autowired
private Mapper mapper;
#Override
public void beforeTestClass(TestContext testContext) {}
... some code...
}
Test Class: Note: AppConfig contains the Mapper Bean defined.
#RunWith(SpringJUnit4ClassRunner.class)
#TestExecutionListeners(listeners = {DependencyInjectionTestExecutionListener.class, CustomExecutionListener.class})
#ContextConfiguration(classes = {AppConfig.class})
public class AccountControllerTest {
....
}
Dependency injection is not supported for TestExecutionListener instances.
Dependency injection is only supported for test instances.
Thus, if your CustomExecutionListener needs to access a bean from the ApplicationContext, it will have to look it up manually -- for example, like this:
public void beforeTestClass(TestContext testContext) {
Mapper mapper = testContext.getApplicationContext().getBean(Mapper.class);
// ... some code...
}
Regards,
Sam (author of the Spring TestContext Framework)
You can also try this: Mapper mapper = Mappers.getMapper(Mapper.class);

Spring test #ContextConfiguration and static context

I have the following piece of code for my abstract test class (I know XmlBeanFactory with ClassPathResource is deprecated, but it's unlikely to be the case of the problem).
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
public abstract class AbstractIntegrationTest {
/** Spring context. */
protected static final BeanFactory context = new XmlBeanFactory(new ClassPathResource(
"com/.../AbstractIntegrationTest-context.xml"));
...
}
It loads the default test configuration XML file AbstractIntegrationTest-context.xml (and then I use autowiring). I also need to use Spring in static methods annotated with #BeforeClass and #AfterClass, so I have a separate context variable pointing to the same location. But the thing is that this is a separate context, which will have different instances of beans. So how can I merge these contexts or how can I invoke Spring's bean initialization defined by #ContextConfiguration from my static context?
I have in mind a possible solution by getting rid of those static members, but I'm curious, if I can do it with relatively small changes to the code.
You are right, your code will produce two application contexts: one will be started, cached and maintained for you by #ContextConfiguration annotation. The second context you create yourself. It doesn't make much sense to have both.
Unfortunately JUnit is not very well suited for integration tests - mainly because you cannot have before class and after class non-static methods. I see two choices for you:
switch to testng - I know it's a big step
encode your setup/tear down logic in a Spring bean included in the context only during tests - but then it will run only once, before all tests.
There are also less elegant approaches. You can use static variable and inject context to it:
private static ApplicationContext context;
#AfterClass
public static afterClass() {
//here context is accessible
}
#Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}
Or you can annotate your test class with #DirtiesContext and do the cleanup in some test bean:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration
#DirtiesContext(classMode = AFTER_CLASS)
public abstract class AbstractIntegrationTest {
//...
}
public class OnlyForTestsBean {
#PreDestroy
public void willBeCalledAfterEachTestClassDuringShutdown() {
//..
}
}
Not sure whether you chose any approach here, but I encounter the same problem and solved it another way using Spring test framework's TestExecutionListener.
There are beforeTestClass and afterTestClass, so both equivalent to #BeforeClass and #AfterClass in JUnit.
The way I do it:
#RunWith(SpringJUnit4ClassRunner.class)
#TestExecutionListeners(Cleanup.class)
#ContextConfiguration(locations = { "/integrationtest/rest_test_app_ctx.xml" })
public abstract class AbstractIntegrationTest {
// Start server for integration test.
}
You need to create a class that extends AbstractTestExecutionListener:
public class Cleanup extends AbstractTestExecutionListener
{
#Override
public void afterTestClass(TestContext testContext) throws Exception
{
System.out.println("cleaning up now");
DomainService domainService=(DomainService)testContext.getApplicationContext().getBean("domainService");
domainService.delete();
}
}
By doing this, you have access to the application context and do your setup/teardown here with spring beans.
Hopefully this help anyone trying to do integration test like me using JUnit + Spring.

Resources