How to test Spring event listener conditional SpEL? - spring

I have a working annotation driven event listener with a conditional statement. But even though the code works fine, I'm not able to unit test this conditional due to a failure in the test case processing the SpEL condition.
I noticed that this error only occurs for Spring Boot 1.5.x version, as 2.1.x version worked as expected. Unfortunately I need to use the 1.5.x version.
Class handling the event:
#Component
public class MyComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(MyComponent.class);
#EventListener(condition = "#createdEvent.awesome")
public void handleOrderCreatedEvent(OrderCreatedEvent createdEvent) {
LOGGER.info("Awesome event handled");
}
}
The event class:
public class OrderCreatedEvent {
public OrderCreatedEvent(boolean awesome) {
this.awesome = awesome;
}
private boolean awesome;
public boolean isAwesome() {
return awesome;
}
}
My test class:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = MyComponent.class)
public class DemoApplicationTests {
#Autowired
private ApplicationEventPublisher publisher;
#MockBean
private MyComponent myComponent;
#Test
public void handleOrderCreatedEvent_shouldExecute_whenAwesome() {
OrderCreatedEvent event = new OrderCreatedEvent(true);
publisher.publishEvent(event);
verify(myComponent).handleOrderCreatedEvent(event);
}
}
Full source code can be found here: https://github.com/crazydevman/spring-event-testing
Running the application everything works as expected. However, when running the test case I keep getting this error:
org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'awesome' cannot be found on null
Debugging the code, it looks like this is due to SpEL not being able to interpret the method parameter name 'createdEvent' for the mocked bean, but I don't know how to fix it.
Is there a way to unit test the event conditional?

#Component
public class MyComponent {
private static final Logger LOGGER = LoggerFactory.getLogger(MyComponent.class);
#EventListener(condition = "#root.args[0].awesome")
public void handleOrderCreatedEvent(OrderCreatedEvent createdEvent) {
LOGGER.info("Awesome event handled");
}
}

Related

I have problem with #Bean WebDriver and How run sevral tests using #Autowrited

Please, promt me. The main idea is create project with Selenium + Spring Boot.
I created it, created test#1 and test#2. If I run test#1, it is okay, after test#1 I want to run test#2, but doing it I have a probem with driver. If I am not mistaken, the reason is #Bean, I created WebDriver using #Bean. I think that problem is the Autowrited Bean with WebDriver, Pls tell me, How create Autowrited Bean for several test.
enter image description here
My Code:1) WebDriver config with Bean
#Configuration
public class WebDriverConfig {
#Bean
#ConditionalOnMissingBean
#ConditionalOnProperty(name = "browser", havingValue = "chrome")
#Primary
public WebDriver chromeDriver() {
WebDriverManager.chromedriver().setup();
return new ChromeDriver();
}
2)BasePage
#Component
public abstract class BasePage {
#Autowired
protected WebDriver driver;
protected final static long WAIT_TIME = 20;
#PostConstruct
private void init() {
PageFactory.initElements(this.driver, this);
}
3)My HomePage with test methods
#Component
public class HomePage extends BasePage {
#FindBy(xpath = "//a[#class='login_btn circle']")
private WebElement singIn;
#FindBy(xpath = "//*[#id='login_input1']")
private WebElement inputLogin;
#FindBy(xpath = "//*[#id='login_input2']")
private WebElement inputPassword;
#FindBy(css = "#login_submit")
private WebElement loginSubmit;
#FindBy(xpath = "//a[.//*[#id='anime_id_17']]")
private WebElement tg;
#FindBy(xpath = "//a[.//*[#id='anime_id_12']]")
private WebElement codeGias;
public HomePage goToHomePage(String url) {
driver.get(url);
waitForPageLoadComplete(WAIT_TIME);
return this;
}
public HomePage goToLogin(String name, String password) {
singIn.click();
waitForPageLoadComplete(WAIT_TIME);
inputLogin.clear();
inputLogin.sendKeys(name);
inputPassword.clear();
inputPassword.sendKeys(password);
loginSubmit.click();
waitForPageLoadComplete(WAIT_TIME);
tg.click();
waitForPageLoadComplete(WAIT_TIME);
return this;
}
public HomePage test(){
waitForPageLoadComplete(WAIT_TIME);
codeGias.click();
waitForPageLoadComplete(WAIT_TIME);
return this;
}
My BaseTest
#ExtendWith(SpringExtension.class)
#SpringBootTest(classes = SpringSelApplication.class)
public class BaseTest {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
#Autowired
public WebDriver driver;
#Autowired
public ApplicationContext applicationContext;
#BeforeEach
public void setup() {
driver.manage().window().maximize();
}
#AfterEach
public void teardown() {
driver.quit();
}
}
Consider the following fixes:
You don't have to use #ExtendWith, the chances are that you're running a reasonably recent version of spring boot and #SpringBootTest is already annotated with #ExtendWith annotation internally
A BasePage class, being abstract is not a #Component by itself.
When using a #SpringBootTest, make sure that you pass the class of the configuration that loads the webdriver. Spring boot, when sees a parameter to the #SpringBootTest doesn't load the whole application and instead loads whatever the the specified configuration class defines. Also since this way talks about "specific" configuration class to be loaded, it stops obeying the usual component scanning rules, so your web page might not be loaded, after all you're mixing configuration styles (you have both classes annotated with #Configuration and those using declarative approach - annotated with #Component)
Consider using #ContextConfiguration instead of #SpringBootTest if you have a plain case of configuration loading and don't really load the whole application

Passing an external property to JUnit's extension class

My Spring Boot project uses JUnit 5. I'd like to setup an integration test which requires a local SMTP server to be started, so I implemented a custom extension:
public class SmtpServerExtension implements BeforeAllCallback, AfterAllCallback {
private GreenMail smtpServer;
private final int port;
public SmtpServerExtension(int port) {
this.port = port;
}
#Override
public void beforeAll(ExtensionContext extensionContext) {
smtpServer = new GreenMail(new ServerSetup(port, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
}
#Override
public void afterAll(ExtensionContext extensionContext) {
smtpServer.stop();
}
}
Because I need to configure the server's port I register the extension in the test class like this:
#SpringBootTest
#AutoConfigureMockMvc
#ExtendWith(SpringExtension.class)
#ActiveProfiles("test")
public class EmailControllerIT {
#Autowired
private MockMvc mockMvc;
#Autowired
private ObjectMapper objectMapper;
#Value("${spring.mail.port}")
private int smtpPort;
#RegisterExtension
// How can I use the smtpPort annotated with #Value?
static SmtpServerExtension smtpServerExtension = new SmtpServerExtension(2525);
private static final String RESOURCE_PATH = "/mail";
#Test
public void whenValidInput_thenReturns200() throws Exception {
mockMvc.perform(post(RESOURCE_PATH)
.contentType(APPLICATION_JSON)
.content("some content")
).andExpect(status().isOk());
}
}
While this is basically working: How can I use the smtpPort annotated with #Value (which is read from the test profile)?
Update 1
Following your proposal I created a custom TestExecutionListener.
public class CustomTestExecutionListener implements TestExecutionListener {
#Value("${spring.mail.port}")
private int smtpPort;
private GreenMail smtpServer;
#Override
public void beforeTestClass(TestContext testContext) {
smtpServer = new GreenMail(new ServerSetup(smtpPort, null, "smtp")).withConfiguration(GreenMailConfiguration.aConfig().withDisabledAuthentication());
smtpServer.start();
};
#Override
public void afterTestClass(TestContext testContext) {
smtpServer.stop();
}
}
The listener is registered like this:
#TestExecutionListeners(value = CustomTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
When running the test the listener gets called but smtpPort is always 0, so it seems as if the #Value annotation is not picked up.
I don't think you should work with Extensions here, or in general, any "raw-level" JUnit stuff (like lifecycle methods), because you won't be able to access the application context from them, won't be able to execute any custom logic on beans and so forth.
Instead, take a look at Spring's test execution listeners abstraction
With this approach, GreenMail will become a bean managed by spring (probably in a special configuration that will be loaded only in tests) but since it becomes a bean it will be able to load the property values and use #Value annotation.
In the test execution listener you'll start the server before the test and stop after the test (or the whole test class if you need that - it has "hooks" for that).
One side note, make sure you mergeMode = MergeMode.MERGE_WITH_DEFAULTS as a parameter to #TestExecutionListeners annotation, otherwise some default behaviour (like autowiring in tests, dirty context if you have it, etc) won't work.
Update 1
Following Update 1 in the question. This won't work because the listener itself is not a spring bean, hence you can't autowire or use #Value annotation in the listener itself.
You can try to follow this SO thread that might be helpful, however originally I meant something different:
Make a GreenMail a bean by itself:
#Configuration
// since you're using #SpringBootTest annotation - it will load properties from src/test/reources/application.properties so you can put spring.mail.port=1234 there
public class MyTestMailConfig {
#Bean
public GreenMail greenMail(#Value(${"spring.mail.port"} int port) {
return new GreenMail(port, ...);
}
}
Now this configuration can be placed in src/test/java/<sub-package-of-main-app>/ so that in production it won't be loaded at all
Now the test execution listener could be used only for running starting / stopping the GreenMail server (as I understood you want to start it before the test and stop after the test, otherwise you don't need these listeners at all :) )
public class CustomTestExecutionListener implements TestExecutionListener {
#Override
public void beforeTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.start();
}
#Override
public void afterTestClass(TestContext testContext) {
GreenMail mailServer =
testContext.getApplicationContext().getBean(GreenMail.class);
mailServer.stop();
}
}
Another option is autowiring the GreenMail bean and using #BeforeEach and #AfterEach methods of JUnit, but in this case you'll have to duplicate this logic in different Test classes that require this behavour. Listeners allow reusing the code.

Dependency injection in custom evaluator in logback in spring boot

I am writing a custom evaluator in which I want to autowire another bean. I am unable to do so as evaluator gets initialized by logger where as beans are initialized by spring context. Below is the sample of my code:
In logback-spring.xml:
<appender name="myAppender" class="ch.qos.logback.classic.net.SMTPAppender">
<evaluator class="com.package.CustomEvaluator">
<marker>FATAL</marker>
<interval>1000000</interval>
</evaluator>
</appender>
My custom evaluator:
#Slf4j
#Component
public class CustomEvaluator extends OnMarkerEvaluator {
#Autowired
private MyService myService;
#Override
public boolean evaluate(ILoggingEvent event) throws EvaluationException {
\\logic goes here
}
}
I am getting object of MyService always as null(which is expected). Is there any work around for this?
It don't think its possible because the Evaluator being an internal logback abstraction is not managed / initialized by spring, so obviously spring can't autowire anything into the evaluator.
In addition note, that logback gets initialized even before application context starts.
Of course you could provide some global holder class for the application context and set the context to it in the main method, and then get the reference to it in this evaluator, something like this:
public class ApplicationContextHolder {
private static ApplicationContext context;
// call it in main() method
public static void setContext(ApplicationContext ctx) {context = ctx;}
public static ApplicationContext getContext() {return context;}
}
class CustomEvaluator extends OnMarkerEvaluator {
public boolean evaluate(ILoggingEvent event) throws EvaluationException {
ApplicationContext ctx = ApplicationContextHolder.getContext();
if(ctx == null) {return false;} // not yet initialized / accessible
MyService myService = ctx.getBean(MyService.class);
}
}
But all-in-all I believe its a very ugly solution.
As a suggestion, I think you should consider refactoring of the logic so that the decision of whether to send an email based on logging event will be taken in the application (which is, I assume, spring boot driven so you have an access to the MyService)
Given the current implementation:
public foo() {
LOGGER.info("This should be sent by email");
}
I suggest a part of application:
#Component
public class MyLogic {
#Autowired MyService myService;
public void foo() {
if(myService.shouldSend()) {
LOGGER.info("This should be sent by email");
}
}
}

AOP using Spring Boot

I am using this Spring AOP code in my Spring Boot starter project in STS. After debugging this for some time I don't see any problem with the AspectJ syntax. The Maven dependencies are generated by STS for a AOP starter project. Is there a glaring omission in this code like an annotation ? The other problem could be with the AOP starter project or with the way I try to test the code in a #PostConstruct method.
I installed AJDT but it appears STS should show AspectJ markers in the IDE on its own. Right ? I don't see the markers. What other AspectJ debugging options are included in STS ? -Xlint is what I used in Eclipse/AJDT.
StateHandler.java
public class StateHandler<EVENTTYPE extends EventType> {
private State<EVENTTYPE> state;
private Event<EVENTTYPE> event;
public StateHandler(State<EVENTTYPE> state, Event<EVENTTYPE> event) {
this.state = state;
this.event = event;
}
public void handle( Event<EVENTTYPE> event ){
state = state.handle( event );
}
public State<EVENTTYPE> getState() {
return state;
}
}
DeviceLogger .java
#Aspect
#Component
public class DeviceLogger {
private static Logger logger = Logger.getLogger("Device");
#Around("execution(* com.devicemachine.StateHandler.*(..))")
public void log() {
logger.info( "Logger" );
}
}
LoggerApplication.java
#SpringBootApplication
public class LoggerApplication {
private static Logger logger = Logger.getLogger("Device");
public static void main(String[] args) {
SpringApplication.run(LoggerApplication.class, args);
}
#PostConstruct
public void log(){
DeviceState s = DeviceState.BLOCKED;
StateHandler<DeviceEvent> sh = new StateHandler<DeviceEvent>( s,
Event.block(DeviceEvent.BLOCKED, "AuditMessage") );
sh.handle(Event.block(DeviceEvent.UNBLOCKED, "AuditMessage"));
}
}
There are 3 obvious things wrong and 1 not so obvious wrong.
Your aspect is wrong and breaks proper method execution. When using an around aspect you must always return Object and use a ProceedingJoinPoint and call proceed() on that.
You are creating new instances of classes yourself, Spring, by default, uses proxy based AOP and will only proxy beans it knows.
In a #PostConstruct method it might be that proxies aren't created yet and that nothing is being intercepted
You need to use class based proxies for that to be enabled add spring.aop.proxy-target-class=true to your application.properties. By default JDK Dynamic Proxies are used which are interface based.
Fix Aspect
Your current aspect doesn't use a ProceedingJoinPoint and as such never does the actual method call. Next to that if you now would have a method that returns a value it would all of a sudden return null. As you aren't calling proceed on the ProceedingJoinPoint.
#Around("execution(* com.devicemachine.StateHandler.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
logger.info( "Logger" );
return pjp.proceed();
}
Create a bean to fix proxying and #PostConstruct
#SpringBootApplication
public class LoggerApplication {
private static Logger logger = Logger.getLogger("Device");
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(LoggerApplication.class, args);
StateHandler<DeviceEvent> sh = context.getBean(StateHandler<DeviceEvent>.class);
sh.handle(Event.block(DeviceEvent.UNBLOCKED, "AuditMessage"));
}
#Bean
public StateHandler<DeviceEvent> auditMessageStateHandler() {
return new StateHandler<DeviceEvent>(DeviceState.BLOCKED, Event.block(DeviceEvent.BLOCKED, "AuditMessage") );
}
}
Add property to enable class proxies
In your application.properties in src\main\resources add the following property with a value of true
spring.aop.proxy-target-class=true

Mocking a service within service (JUnit)

I have the following service:
#Service
public class PlayerValidationService {
#Autowire
private EmailService emailService;
public boolean validatePlayerEmail(Player player) {
return this.emailService.validateEmail(player.getEmail());
}
Now in my junit test class i'm using a different 3rd service that uses PlayerValidationService :
public class junit {
#autowire PlayerAccountService playerAccountService ;
#Test
public test() {
this.playerAccountService .createAccount();
assertAllSortsOfThings();
}
Is it possible to mock the EmailService within the PlayerAccountService when using annotation based autowiring? (for example make the mock not checking the validation of the email via the regular email provider we work with)
thanks.
There are a couple of ways in which you could do this. First the simplest option is to ensure that your service provides a setEmailService(EmailService) method. In which case you just replace the Spring-injected implementation with your own.
#Autowired
private PlayerValidationService playerValidationService;
#Mock
private EmailService emailService;
#Before
public void setup() {
initMocks(this);
playerValidationService.setEmailService(emailService);
}
A shortcoming of that approach is that an instance of the full-blown EmailService is likely to be created by Spring. Assuming that you don't want that to happen, you can use 'profiles'.
In your test packages, create a configuration class which is only active in a particular profile:
#Configuration
#Profile("mockemail")
public class MockEmailConfig {
#Bean(name = "emailService")
public EmailService emailService() {
return new MyDummyEmailService();
}
}
And add an annotation to your test to activate that profile:
#ActiveProfiles({ "mockemail" })
public class PlayerValidationServiceTest {
//...
}

Resources