I'm currently facing an issue with testing RabbitMQ consumers with mocks. The issue seems to be that one test class runs with an application context without any mocks, as expected. The next test class to run sets up some mocks that it expects the consumers to use, however when the test runs and a message is sent and it gets picked up by the non-mocked consumers from the application context created for the first test class. As a result my second test fails.
Here is the first test:
#SpringBootTest
public class DemoApplicationTests extends AbstractTestNGSpringContextTests {
#Autowired
private RabbitAdmin rabbitAdmin;
private Logger logger = LoggerFactory.getLogger(this.getClass());
#Test(priority = 1)
public void contextLoads() {
logger.info("=============== CONSUMERS: " + rabbitAdmin.getQueueProperties(USER_MESSAGING_QUEUE).get(RabbitAdmin.QUEUE_CONSUMER_COUNT));
}
}
Second test:
#SpringBootTest
public class UserServiceTests extends AbstractTestNGSpringContextTests {
#Autowired
private UserService userService;
#Autowired
private UserMessageConsumer userMessageConsumer;
#MockBean
#Autowired
private ThirdPartyUserDataClient thirdPartyUserDataClient;
#Autowired
private UserRepository userRepository;
#Autowired
private RabbitAdmin rabbitAdmin;
#Test(priority = 2)
public void createUpdateUserTest() {
logger.info("=============== CONSUMERS: " + rabbitAdmin.getQueueProperties(USER_MESSAGING_QUEUE).get(RabbitAdmin.QUEUE_CONSUMER_COUNT));
String additionalData = org.apache.commons.lang3.RandomStringUtils.random(5);
Mockito.when(thirdPartyUserDataClient.getAdditionalUserData(ArgumentMatchers.anyLong())).thenReturn(additionalData);
User user = new User();
user.setName("Test User");
user.setState(UserState.PENDING);
user = userService.createUser(user);
Assert.assertNotNull(user.getId());
User finalUser = user;
Awaitility.await().until(() -> {
User user2 = userService.getUserById(finalUser.getId());
return finalUser != null && additionalData.equals(user2.getAdditionalData());
});
user.setState(UserState.CREATED);
user = userService.updateUser(user);
Assert.assertEquals(UserState.CREATED, user.getState());
}
}
The consumer:
#Component
public class UserMessageConsumer {
private Logger logger = LoggerFactory.getLogger(this.getClass());
public static final String FAILED_TO_GET_ADDITIONAL_DATA = "FAILED_TO_GET_ADDITIONAL_DATA";
#Autowired
private UserService userService;
#Autowired
private ThirdPartyUserDataClient thirdPartyUserDataClient;
public void handleUserCreatedMessage(UserCreatedMessage userCreatedMessage) {
Long userId = userCreatedMessage.getUserId();
User user = userService.getUserById(userId);
if (user != null) {
String additionalData;
try {
additionalData = thirdPartyUserDataClient.getAdditionalUserData(userId);
logger.info("Successfully retrieved additional data [{}] for user [{}].", additionalData, userId);
} catch (HttpClientErrorException ex) {
additionalData = FAILED_TO_GET_ADDITIONAL_DATA;
logger.warn("Failed to retrieve additional data for user [{}].", userId, ex);
}
user.setAdditionalData(additionalData);
userService.updateUser(user);
}
}
}
This brings up two related questions:
How am I supposed to properly do mock bean testing with consumers in
Spring?
It looks like Spring is bringing up a new a
ApplicationContext for each test class, indicated by the consumer count increasing on the subsequent test runs. It appears
that #MockBean affects the cache key of the ApplicationContext (see:
Mocking and Spying Beans in Spring Boot) and likely explains why there are multiple application contexts.
But how do I stop the consumers in the other stale application contexts from
consuming my test messages?
I've bugjar'd this issue here: RabbitMQ MockBean BugJar
Add #DirtiesContext to each test class to shut down the cached context.
Related
I have the following publishing class.
#Component
public class Publisher {
#Autowired
private MessageChannel publishingChannel;
#Override
public void publish(Event event) {
publishingChannel.send(event);
}
}
I have the following test class.
#RunWith(SpringRunner.class)
#SpringBootTest
public class PublisherTest {
private final List<Event> events = new ArrayList<>();
#Autowired
private Publisher publisher;
#Test
public void testPublish() {
Event testEvent = new Event("some_id_number");
publisher.publish(testEvent);
Awaitility.await()
.atMost(2, TimeUnit.SECONDS)
.until(() -> !this.events.isEmpty());
}
#ServiceActivator(inputChannel = "publishingChannel")
public void publishEventListener(Event event) {
this.events.add(event);
}
}
The message channel bean is instantiated elsewhere. The publisher runs as expected and an event is publishing to the channel, however the service activator is never invoked. What am I missing here?
Turns out you need to move the service activator to a separate test component class (#TestComponent prevents this from being injected outside the test context).
#TestComponent
public class TestListener {
public final List<Object> results = new ArrayList<>();
#ServiceActivator(inputChannel = "messageChannel")
public void listener(Event event) {
Object id = event.getHeaders().get("myId");
results.add(id);
}
}
Then you can bring this listener into your test. Make sure you use #Import to bring your service activator class into the test context.
#SpringBootTest
#Import(TestListener.class)
class PublisherTest {
#Autowired
private Publisher publisher;
#Autowired
private TestListener testListener;
#Test
void testPublish() {
this.publisher.publish(new Event().addHeader("myId", 1));
Awaitility.await()
.atMost(2, TimeUnit.SECONDS)
.until(() -> !this.testListeners.results.isEmpty());
}
}
The test passes after making these changes. Figured this out with a demo app and applied it to a production testing issue.
I have exception-messages written down in the application.yml. They are pure text, which is later reformatted using java.text.MessageFormat.
I have got the following custom RuntimeException my service throws when login failed:
#Component
public class AccountLoginFailedException extends RuntimeException {
#Autowired
public AccountLoginFailedException(#Value("#(${authservice.exception-messages.login-failed})") final String message, #Qualifier(value = "Credentials") final Credentials credentials) {
super(MessageFormat.format(message, credentials.getUsername()));
}
}
My test, which solely tests the AccountController and mocks away the service behind it:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = AuthServiceTestConfiguration.class)
#WebMvcTest(AccountController.class)
public class AccountControllerTest {
#Autowired
private BeanFactory beanFactory;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private TestHelper helper;
#MockBean
private AccountService accountService;
#Autowired
private JwtService jwtService;
#Autowired
private MockMvc mvc;
#Test
public void test_LoginFailed_AccountDoesNotExist() throws Exception {
// Given
final Credentials credentials = helper.testCredentials();
final String credentialsJson = objectMapper.writeValueAsString(credentials);
final AccountLoginFailedException loginFailedException = beanFactory.getBean(AccountLoginFailedException.class, credentials);
// When
given(accountService.login(credentials)).willThrow(loginFailedException);
// Then
mvc
.perform(
post("/login")
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(credentialsJson))
.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.data").value(equalTo(loginFailedException.getMessage())));
}
}
message contains the correct String. However: credentials contains just an empty object (not null) instead of the one created using helper.testCredentials().
Here is a slightly simplified TestHelper class I am using:
#TestComponent
public class TestHelper {
public static final String USERNAME = "SomeUsername";
public static final String PASSWORD = "SomePassword";
#Autowired
private BeanFactory beanFactory;
public Credentials testCredentials() {
final Credentials credentials = beanFactory.getBean(Credentials.class.getSimpleName(), Credentials.class);
credentials.setUsername(USERNAME);
credentials.setPassword(PASSWORD);
return credentials;
}
}
These custom exceptions are thrown by my application only and are always expected to contain the credentials (username) responsible for it. I also have a AccountExceptionsControllerAdvice-class, which just wraps these custom exceptions in a generic JSON response, exposing the error in a preferred manner.
How can I ensure that this particular instance of Credentials is inserted into the particular instance of AccountLoginFailedException? Or should I not be autowiring exceptions at all?
You could mock your Credentials component in your tests as follows:
#MockBean
private Credentials credentials;
#Before
public void before() {
when(credentials.getUsername()).thenReturn(USERNAME);
when(credentials.getPassword()).thenReturn(PASSWORD);
}
I'm building a SpringBoot CRUD application based on a REST controller calling a Spring Service. The POJO received have validation-related annotations (including custom validators) and the actual validations are triggered inside the Service (see below).
Everything works perfectly fine in a SpringBoot execution.
I now need to build relevant unit test cases for my Service i.e I do not want to start an application server via a SpringBootRunner.
Below is my Patient class with validation-related annotations.
#Data
#Entity
public class Patient {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#NotEmpty(message = "patient.name.mandatory")
private String name;
#Past(message = "patient.dateOfBirth.inThePast")
#NotNull(message = "patient.dateOfBirth.mandatory")
private Date dateOfBirth;
private boolean isEmergency;
// [...]
}
This is the Service called by SpringBoot's REST controller.
#Service
#Validated
public class PatientService {
#Autowired
private PatientRepository repository;
#Autowired
private Validator validator;
public Patient create(Patient patient) {
if (! patient.isEmergency()) {
validator.validate(patient);
// then throw exception if validation failed
}
// [...]
}
}
And here is my JUnit test.
#RunWith(MockitoJUnitRunner.class)
#SpringBootTest
#Import(ModalityTestConfiguration.class)
public class PatientServiceTest {
#InjectMocks
private PatientService service;
#MockBean
private PatientRepository repository;
#Autowired
private Validator validator;
#Test
public void invalidEmptyPatientNoEmergency() {
Patient p = new Patient();
Patient result = null;
try {
result = service.create(p); // validations must fail -> exception
assert(false);
} catch (ConstraintViolationException e) {
// Execution should get here to verify that validations are OK
assert(result != null);
assert(e.getConstraintViolations() != null);
assert(e.getConstraintViolations().size() != 0);
// [...]
} catch (Exception e) {
assert(false);
}
}
Just in case, here is the REST controller (not relevant I think as not related to the JUnit test)
#RestController
public class PatientController {
#Autowired
private PatientService patientService;
#PostMapping("/patients")
Patient createPatient(#RequestBody Patient patient) {
return (patientService.create(patient));
}
My problem is that the Validator in my Service is always null.
I have tried creating a dedicated configuration file with a validator bean
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
and linking the configuration to the Test case
#ContextConfiguration(locations = {"/modality-test-config.xml"})
I have tried to define a local #Bean for the Validator
I have played around with and without #Autowire
I have changed the #RunWith
I'm sure it must be a minor detail somewhere but I just don't seem to get a not-null validator inside the JUnit test of the Service.
UPDATE
Here is the TestConfiguration class I have added following TheHeadRush comment.
#TestConfiguration
public class ModalityTestConfiguration {
#Bean("validator")
public Validator validator() {
return (Validation.buildDefaultValidatorFactory().getValidator());
}
}
I have also added the corresponding #Import annotation in the Test class above.
Still no luck: the Validator field remains null in both the Test class and in the Service. Also, the breakpoint in the TestConfiguration class doesn't seem to get called.
I want to write a simple test using #RestClientTest for the component below (NOTE: I can do it without using #RestClientTest and mocking dependent beans which works fine.).
#Slf4j
#Component
#RequiredArgsConstructor
public class NotificationSender {
private final ApplicationSettings settings;
private final RestTemplate restTemplate;
public ResponseEntity<String> sendNotification(UserNotification userNotification)
throws URISyntaxException {
// Some modifications to request message as required
return restTemplate.exchange(new RequestEntity<>(userNotification, HttpMethod.POST, new URI(settings.getNotificationUrl())), String.class);
}
}
And the test;
#RunWith(SpringRunner.class)
#RestClientTest(NotificationSender.class)
#ActiveProfiles("local-test")
public class NotificationSenderTest {
#MockBean
private ApplicationSettings settings;
#Autowired
private MockRestServiceServer server;
#Autowired
private NotificationSender messageSender;
#Test
public void testSendNotification() throws Exception {
String url = "/test/notification";
UserNotification userNotification = buildDummyUserNotification();
when(settings.getNotificationUrl()).thenReturn(url);
this.server.expect(requestTo(url)).andRespond(withSuccess());
ResponseEntity<String> response = messageSender.sendNotification(userNotification );
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private UserNotification buildDummyUserNotification() {
// Build and return a sample message
}
}
But i get error that No qualifying bean of type 'org.springframework.web.client.RestTemplate' available. Which is right of course as i havn't mocked it or used #ContextConfiguration to load it.
Isn't #RestClientTest configures a RestTemplate? or i have understood it wrong?
Found it! Since i was using a bean that has a RestTemplate injected directly, we have to add #AutoConfigureWebClient(registerRestTemplate = true) to the test which solves this.
This was in the javadoc of #RestClientTest which i seem to have ignored previously.
Test which succeeds;
#RunWith(SpringRunner.class)
#RestClientTest(NotificationSender.class)
#ActiveProfiles("local-test")
#AutoConfigureWebClient(registerRestTemplate = true)
public class NotificationSenderTest {
#MockBean
private ApplicationSettings settings;
#Autowired
private MockRestServiceServer server;
#Autowired
private NotificationSender messageSender;
#Test
public void testSendNotification() throws Exception {
String url = "/test/notification";
UserNotification userNotification = buildDummyUserNotification();
when(settings.getNotificationUrl()).thenReturn(url);
this.server.expect(requestTo(url)).andRespond(withSuccess());
ResponseEntity<String> response = messageSender.sendNotification(userNotification );
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
private UserNotification buildDummyUserNotification() {
// Build and return a sample message
}
}
I have 2 DataSources in my app.
So, to get the required JdbcTemplate, i use #Qualifier. But, when i do like below, the test runs... but stays waiting indefinitely, if there is any use of JdbcTemplate in the "Method Under Test".
#Service
#Transactional
public class SampleDatabaseService {
#Autowired
#Qualifier("firstDbJdbcTemplate")
private JdbcTemplate firstDbJdbcTemplate;
#Autowired
#Qualifier("secondDbJdbcTemplate")
private JdbcTemplate secondDbJdbcTemplate;
#Cacheable("status")
public Map<String, Device> readAllValidDeviceStatus() {
Map<String, Device> allDeviceStatuses = new HashMap<>();
//Stops at below line indefinitely if "SpyBean" is used
List<StatusDetail> statusDetails = firstDbJdbcTemplate
.query(SqlQueries.READ_DEVICE_STATUS, BeanPropertyRowMapper.newInstance(StatusDetail.class));
statusDetails
.stream()
.filter(deviceStatus -> deviceStatus.getName() != "Some Invalid Name")
.forEach(deviceStatus -> allDeviceStatuses
.put(deviceStatus.getName(), buildDevice(deviceStatus)));
return allDeviceStatuses;
}
/** More Stuff **/
}
and the Test :
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#Transactional
#Rollback
#ActiveProfiles("test")
public class SampleDatabaseServiceTest {
#SpyBean
#Qualifier("firstDbJdbcTemplate")
private JdbcTemplate firstDbJdbcTemplate;
#Autowired
private SampleDatabaseService serviceUnderTest;
#Before
public void populateTables() {
//Insert some Dummy Records in "InMemory HSQL DB" using firstDbJdbcTemplate
}
#Test
public void testReadAllValidDeviceStatus() {
// When
Map<String, Device> allDeviceStatuses = serviceUnderTest.readAllValidDeviceStatus();
// Then
assertThat(allDeviceStatuses).isNotNull().isNotEmpty();
// More checks
}
/* More Tests */
}
But, when i replace the #SpyBean with #Autowired in Test, it works fine.
Why is it so? Any help is greatly appreciated. :)
Use it in below format
#MockBean(name = "firstDbJdbcTemplate")
private JdbcTemplate firstDbJdbcTemplate;