Spring Boot unit test constructor injection - java

I'm using Spring Boot to create a REST API and write some unit tests on my controllers.
I know that the recommended manner to inject beans in spring is the constructor injection.
But when i add the #SpringBootTest annotation to my test class, I can not inject my controller class with constructor, I find myself obliged to use #Autowired.
Have some explanation and is there another way to use constructor injection with SpringBootTest.
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class PersonControllerTest {
#LocalServerPort
private int port;
#Autowired
private PersonController controller;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void greetingShouldReturnDefaultMessage() throws Exception {
assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/cvtech/Persons/",
String.class)).contains("content");
}
#Test
public void contextLoads() throws Exception {
assertThat(controller).isNotNull();
}
#Test
void findAllByJob() {
}
}

For those using Kotlin, using field-injection means having to use lateinit var fields. Which is far from ideal.
It is possible to use constructor injection on SpringBoot tests however, using the #TestConstructor:
#ExtendWith(SpringExtension::class)
#TestConstructor(autowireMode = ALL)
#SpringBootTest(
classes = [MyApplication::class],
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
)
internal class MyIntegrationTest(
val beanA: BeanA,
#Qualifier("some qualified") val beanB: BeanB,
) {
...
// Tests can use the beans injected in the constructor without any problems
...
}

It's fine for your test to use field injection as the Test itself is not part of your domain; the test won't be part of your application context.
Also
You don't want to use SpringBootTest to test a controller, because that will wire ALL beans which can be way too heavy and time-consuming. Instead, you probably only want to create your controller and it's dependencies.
So your best option is to use #WebMvcTest which will only create the beans required for testing the specified controller.
#ExtendWith(SpringExtension.class)
#WebMvcTest(controllers = PersonController.class)
class PersonControllerTest {
#Autowired
private MockMvc mockMvc;
#Test
public void greetingShouldReturnDefaultMessage() throws Exception {
mockMvc.perform(get("/cvtech/Persons"))
.andExpect(status().isOk())
.andExpect(content().string(contains("content")));
}
}
Note that #WebMvcTest will search for a class annotated with #SpringBootConfiguration as it's default configuration. If it does not find it, or you want to manually specify some configuration classes, also annotate the test with #ContextConfiguration.
Also, as a sidenote, when using TestRestTemplate, you don't need to specify host and port. Just call restTemplate.getForObject("/cvtech/persons", String.class));
Same when using MockMvc or WebTestClient.

Related

Mockito: How to mock spring special DI that the injected object doesn't have no-arg constructor

I'm using Mockito 3.4.6 in unit test, actually, i have integrated Mockito to my unit test and it works well. While, now i need to optimize some unit test, it's a special dependency injection that the injected object doesn't have no-arg constructor, I tried #Spy but it didn't work.
My Test: I tried 1. #Spy; 2. #Spy with setting instance using = getDtInsightApi(); 3. #Spy with #InjectMocks, all of tests are failed. As Mockito docs said, seems it can't work for this case.
#InjectMocks Mockito will try to inject mocks only either by constructor injection,
setter injection, or property injection in order and as described below.
Also if only use #Spy, it will throw MockitoException:
org.mockito.exceptions.base.MockitoException:
Failed to release mocks
This should not happen unless you are using a third-part mock maker
...
Caused by: org.mockito.exceptions.base.MockitoException: Unable to initialize #Spy annotated field 'api'.
Please ensure that the type 'DtInsightApi' has a no-arg constructor.
...
Caused by: org.mockito.exceptions.base.MockitoException: Please ensure that the type 'DtInsightApi' has a no-arg constructor.
See my pseudocode as below:
configure class:
#Configuration
public class SdkConfig {
#Resource
private EnvironmentContext environmentContext;
#Bean(name = "api")
public DtInsightApi getApi() {
DtInsightApi.ApiBuilder builder = new DtInsightApi.ApiBuilder()
.setServerUrls("sdkUrls")
return builder.buildApi();
}
}
DtInsightApi class with no public no-arg constructor and get instance by its inner class
public class DtInsightApi {
private String[] serverUrls;
DtInsightApi(String[] serverUrls) {
this.serverUrls = serverUrls;
}
// inner class
public static class ApiBuilder {
String[] serverUrls;
public ApiBuilder() {
}
...code...
public DtInsightApi buildApi() {
return new DtInsightApi(this.serverUrls);
}
}
...code...
}
unit test class:
public Test{
#Autowired
private PendingTestService service;
#Spy
private Api api = getDtInsightApi();
#Mock
private MockService mockService;
#Before
public void setUp() throws Exception {
// open mock
MockitoAnnotations.openMocks(this);
// i use doReturn(...).when() for #Spy object
Mockito.doReturn(mockService).when(api)
.getSlbApiClient(MockService.class);
Mockito.when(mockService.addOrUpdate(any(MockDTO.class)))
.thenReturn(BaseObject.getApiResponseWithSuccess());
}
public DtInsightApi getDtInsightApi () {
return new DtInsightApi.ApiBuilder()
.setServerUrls(new String[]{"localhost:8080"})
.buildApi();
}
#Test
public void testUpdate() {
service.update();
}
}
PendingTestService:
#Service
public class PendingTestService{
#Autowired
DtInsightApi api;
public void update() {
// here mockService isn't the object i mocked
MockService mockService = api.getSlbApiClient(MockService.class);
mockService.update();
}
}
Question: How to mock the DI object DtInsightApi which doesn't have no-arg constructor.
After checked Spring docs about unit test, I found a solution using #MockBean.
Spirng docs:https://docs.spring.io/spring-boot/docs/1.5.2.RELEASE/reference/html/boot-features-testing.html
According to Spring docs, you can use #MockBean to mock a bean inside your ApplicationContext, so i can use #MockBean to mock DtInsightApi.
It’s sometimes necessary to mock certain components within your application context when running tests. For example,
you may have a facade over some remote service that’s unavailable during development. Mocking can also be useful when
you want to simulate failures that might be hard to trigger in a real environment.
Spring Boot includes a #MockBean annotation that can be used to define a Mockito mock for a bean inside your ApplicationContext.
You can use the annotation to add new beans, or replace a single existing bean definition. The annotation can be used directly on test classes,
on fields within your test, or on #Configuration classes and fields. When used on a field, the instance of the created mock will also be injected.
Mock beans are automatically reset after each test method.
My Solution: Use #MockBean and BDDMockito.given(...).willReturn(...), use
#Qualifier("api") to specify the bean name because #MockBean injected by class type, if you have same class beans, you need to specify bean name.
My code in test class:
public class Test{
#MockBean
#Qualifier("api")
private DtInsightApi api;
#Mock
private MockService mockService;
#Before
public void setUp() throws Exception {
// open mock
MockitoAnnotations.openMocks(this);
BDDMockito.given(this.api.getSlbApiClient(MockService.class)).willReturn(mockService);
}
#Autowired
private PendingTestService service;
#Test
public void testUpdate() {
service.update();
}
}
Debug the mockService you can see mockService instance is generated by Mockito, mock succeed.
You can also refer to Spring docs example: mock RemoteService in unit test.
import org.junit.*;
import org.junit.runner.*;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.context.*;
import org.springframework.boot.test.mock.mockito.*;
import org.springframework.test.context.junit4.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
#RunWith(SpringRunner.class)
#SpringBootTest
public class MyTests {
#MockBean
private RemoteService remoteService;
#Autowired
private Reverser reverser;
#Test
public void exampleTest() {
// RemoteService has been injected into the reverser bean
given(this.remoteService.someCall()).willReturn("mock");
String reverse = reverser.reverseSomeCall();
assertThat(reverse).isEqualTo("kcom");
}
}

Can't mock repository when testing with mockmvc

I have this quite simple controller class and a simple (jpa) repository.
What I want to do is to test it's api but mock it's repository and let it return an object or not depending on the test case.
My problem now is that I don't know how to do that.
I know how to mock a repository and inject it to a controller/service class with #Mock / #InjectMocks / when().return()
But I fail when I want to do the same after doing a request with MockMvc.
Any help is highly appreciated
The controller
import java.util.Optional;
#RestController
#Slf4j
public class ReceiptController implements ReceiptsApi {
#Autowired
private ReceiptRepository receiptRepository;
#Autowired
private ErrorResponseExceptionFactory exceptionFactory;
#Autowired
private ApiErrorResponseFactory errorResponseFactory;
#Override
public Receipt getReceipt(Long id) {
Optional<ReceiptEntity> result = receiptRepository.findById(id);
if (result.isEmpty()) {
throw invalid("id");
}
ReceiptEntity receipt = result.get();
return Receipt.builder().id(receipt.getId()).purchaseId(receipt.getPurchaseId()).payload(receipt.getHtml()).build();
}
private ErrorResponseException invalid(String paramName) {
return exceptionFactory.errorResponse(
errorResponseFactory.create(HttpStatus.NOT_FOUND.value(), "NOT_VALID", String.format("receipt with id %s not found.", paramName))
);
}
}
And it's test class
#WebMvcTest(ReceiptController.class)
#RestClientTest
public class ReceiptControllerTest {
#InjectMocks
private ReceiptController receiptController;
#Mock
private ReceiptRepository receiptRepository;
#Mock
private ErrorResponseExceptionFactory exceptionFactory;
#Mock
private ApiErrorResponseFactory errorResponseFactory;
private MockMvc mvc;
#Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mvc = MockMvcBuilders.standaloneSetup(
new ReceiptController())
.build();
}
#Test
public void getReceiptNotFoundByRequest() throws Exception {
mvc.perform(MockMvcRequestBuilders
.get("/receipt/1")
.header("host", "localhost:1348")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}
//TODO: Finish this test
#Test
public void getReceiptFoundByRequest() throws Exception {
ReceiptEntity receipt1 = ReceiptEntity.builder().id(99999L).purchaseId(432L).html("<html>").build();
when(receiptRepository.findById(1L)).thenReturn(Optional.of(ReceiptEntity.builder().id(1L).purchaseId(42L).html("<html></html>").build()));
ResultActions result = mvc.perform(get("/receipt/1")
.header("host", "localhost:1348")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
}
Within your setUp() method, you're using Mockito to mock the beans annotated with #Mock and inject them in a new instance of ReceiptController, which is then put into the field annotated with #InjectMocks.
On the next line, you're setting up MockMvc with a new instance of ReceiptController (you use new ReceiptController()), rather than using the instance that Mockito created. That means that all fields within that instance will be null.
This pretty much boils down exactly to Why is my Spring #Autowired field null.
To solve this, you could pass receiptController to MockMvc. In that case, you could also remove the #WebMvcTest and #RestClientTest as you're not using them.
Alternatively, you could setup your test with #RunWith(SpringRunner.class), and properly set up a Spring test by using #Autowired in stead of #InjectMocks and #MockBean in stead of #Mock. In that case, you don't need a setUp() method at all, as MockMvc could be autowired by Spring.
#WebMvcTest and MockMvc allows you to do integration testing, not unit testing.
They allow you to boot an actual Spring application (using a random port) and test the actual controller class with its dependencies. This means that the variables you declared at the top of your test are not actually used in your current setup.
If you wish to unit-test your controller, you can remove #WebMvcTest, create mock dependencies (like you did) and call your methods directly instead of using MockMvc.
If you really wish to write an integration test, you need to mock your external dependencies (the database for example). You can configure spring to use an embedded database (H2 for example) in the test environment, so that you do not affect your real database.
See an example here : https://www.baeldung.com/spring-testing-separate-data-source

Is there a way to use Autowired constructor in JUnit test using Spring or Spring Boot?

Assume, that I have a test configuration with several Spring beans, that are actually mocked and I want to specify the behavior of those mocks inside JUnit test suite.
#Profile("TestProfile")
#Configuration
#EnableTransactionManagement
#ComponentScan(basePackages = {
"some.cool.package.*"})
public class IntegrationTestConfiguration {
#Bean
#Primary
public Cool cool() {
return Mockito.mock(Cool.class);
}
}
// ...
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
#ActiveProfiles("TestProfile")
public class CoolIntegrationTest {
private final Cool cool;
#Autowired
public CoolIntegrationTest(Cool cool) {
this.cool = cool;
}
#Test
public void testCoolBehavior {
when(cool.calculateSomeCoolStuff()).thenReturn(42);
// etc
}
}
If I run this test I will get:
java.lang.Exception: Test class should have exactly one public zero-argument constructor
I know the workaround like use Autowired fields in tests, but I wonder if there a way to use Autowired annotation in JUnit tests?
It's not the autowiring that's the problem, it's the no-arg constructor. JUnit test classes should have a single, no argument constructor. To achieve what you are attempting to do, you should do the following:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest
#ActiveProfiles("TestProfile")
#ContextConfiguration(classes = {IntegrationTestConfiguration.class})
public class CoolIntegrationTest {
#Autowired
private final Cool cool;
#Test
public void testCoolBehavior {
when(cool.calculateSomeCoolStuff()).thenReturn(42);
// etc
}
}
The contextConfiguration annotation tells spring which config to use for the test, and autowiring the field instead of the constructor will allow you to test your spring bean.
To run a test using Spring you have to add #RunWith(SpringRunner.class) and make sure that your class is added to the classpath. There are a few ways to do it. I.e.
Add class to MVC configuration #WebMvcTest({Class1.class, Class2.class}) or use #ContextConfiguration.
But I see your code, I suppose that it would be easier just use #Mock or #MockBean to mock your beans. It will be much easier.
JUnit requires the Test case to have a no-arg constructor, so, since you don't have one, the exception happens before the wiring process.
So Constructor-Autowiring just doesn't work in this case.
So what to do?
There are many approaches:
The easiest one (since you have spring) is taking advantage of #MockBean annotation:
#RunWith(SpringRunner.class)
#SpringBootTest
....
class MyTest {
#MockBean
private Cool cool;
#Test
void testMe() {
assert(cool!= null); // its a mock actually
}
}
Besides args constructor you need to have additional one no-args constructor. Try add it and check if this exception still occurcs.
#Autowired
public CoolIntegrationTest(Cool cool) {
this.cool = cool;
}
public CoolIntegrationTest() {}

How to integrate Spring Boot, Cucumber and Mockito?

There are ways online to integrate Cucumber with Spring Boot. But I cannot find how to do so with Mockito also. If I use the Cucumber runner and annotate the steps file with ContextConfiguration and SpringBootTest, the container injects the Autowired dependencies and its all fine. The problem is that dependencies annotated with Mock, MockBean and InjectMocks dont work. Anyone knows why it doesnt work and how to make it work?
EDIT: It is possible to instantiate the bean with mock(Bean.class), instead of using the Mock annotation. But what about features like MockBean and InjectMocks?
Runner
#RunWith(Cucumber.class)
#CucumberOptions(plugin = {"pretty", "html:build/cucumber_report/"},
features = "classpath:cucumber/",
glue = {"com.whatever"},
monochrome = true,
dryRun = false)
public class CucumberTest {
}
Steps
#ContextConfiguration
#SpringBootTest
public class CucumberSteps
{
#Autowired
private Bean bean;
#InjectMocks //doesnt work
private AnotherBean anotherBean;
#MockBean //doesnt work with #Mock also
MockedBean mockedBean;
#Given("^Statement$")
public void statement() throws Throwable {
MockitoAnnotations.initMocks(this); //doesnt work with or without this line
Mockito.when(mockedBean.findByField("value"))
.thenReturn(Arrays.asList());
}
//Given-When-Then
}
Runner :
#CucumberOptions(plugin = {"pretty"},
glue = {"com.cucumber.test"},
features = "x/y/resources")
public class CucumberTest {
}
Here we will create a class with #SpringBootTest, #RunWith(SpringRunner.class) to start load beans into spring context. Now will mock the spring beans whatever we want to mack at here
#RunWith(SpringRunner.class)
#SpringBootTest
public class SpringTest {
#MockBean
private Mockedbean mockedbean;
}
Now, we need to extend the SpringBootTest annotated testclass to CucumberSteps class, then autowire the mocked bean at here, will get the instance of mocked bean(Mockedbean). We can do autowire and get instance of other spring boot beans also(TestBean)
public class CucumberSteps extends SpringTest {
#Autowired
private Mockedbean mockedbean;
#Autowired
private TestBean testBean;
}

Unit test in Spring: injecting a dependency into a component under test

I have a very simple rest controller:
#RestController
public class MyController {
#Autowired
public Logger logger;
The logger dependency gets injected via the following configuration:
#Configuration
public class MyConfig {
#Bean
public Logger logger() {
return LoggerFactory.getLogger(MyController.class);
}
If I run the Spring application that contains the controller then everything works fine. However, I cannot manage to achieve this dependency injection when running my unit tests. In this case I have the following test configuration:
#Configuration
#Profile("test")
public class MyTestConfig {
#Bean
public Logger logger() {
return LoggerFactory.getLogger(MyCOntroller.class);
}
And this is the relevant part of my unit tests code:
#RunWith(MockitoJUnitRunner.class)
#ContextConfiguration(classes = MyTestConfig.class)
#ActiveProfiles("test")
public class MyContollerTest {
However the logger object does not get "autowired" in MyController (note that I do not want to mock the logger object), which results in a null pointer reference.
What am I missing?
A unit test shouldn't use any Spring configuration. You should simply instantiate your component, and inject dependencies (usually fake ones) manually.
You used field injection, which makes it a bit harder. With constructor injection, all you would need to do is
Logger logger = LoggerFactory.getLogger(MyController.class);
MyController controller = new MyController(logger);
Mockito can help injecting fake dependencies for you, though, even when using field injection, thanks to the #Mock, #Spy and #InjectMocks annotations:
#Spy
private Logger logger = LoggerFactory.getLogger(MyController.class);
#InjectMocks
private MyController controller;
#Before
public void prepare() {
MockitoAnnotations.initMocks(this);
}
That said, if I'm not mistaken, you're not using #RunWith(SpringJUnit4ClassRunner.class), so your test runner doesn't know anything about Spring, and thus doesn't create or use any Spring configuration.

Categories

Resources