Problem:
To test the application code with minimal mocking effort, we want to make the payment processor implementation configurable so that the application can use the dummy processor if none is configured.
Solutions:
1- Factory Method In The Configuration Class
With this solution, you can create beans conditionally by a factory method created in the configuration class. You can create an instance of the desired implementation based on a system property and an instance of fallback implementation otherwise. It doesn’t create extra beans.
@Configuration
public class AppConfig {
private final Environment environment;
public AppConfig(Environment environment) {this.environment = environment;}
@Bean
public PaymentProcessor paymentProcessor() {
if (environment.getProperty("payment.processor.enabled", Boolean.class, false)) {
return new PayuPaymentProcessor();
} else {
return new NoopPaymentProcessor();
}
}
}
Let’s test it and see how it works.
import org.example.AppConfig;
import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class ConditionalDiTest {
@Test
public void testWithNoopPaymentProcessorImp() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
}
@Test
public void testWithCurrentPaymentProcessorImpl() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment environment = context.getEnvironment();
environment.getPropertySources()
.addFirst(new MapPropertySource("test", Map.of("payment.processor.enabled", "true")));
context.register(AppConfig.class);
context.refresh();
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
}
}
2. Conditional Annotation
The @Conditional annotation allows the registration of beans based on custom conditions. It can be defined at the class or method level. You can define the logic that determines whether the bean should be created by creating a class that implements org.springframework.context.annotation.Condition
interface.
To implement this solution, let’s create our condition classes like the ones below:
public class PaymentProcessorEnabledCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("payment.processor.enabled", Boolean.class, false);
}
}
public class PaymentProcessorFallbackCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return !context.getEnvironment().getProperty("payment.processor.enabled", Boolean.class, false);
}
}
Add @Conditional annotation to the implementation classes and pass the appropriate condition classes as arguments.
@Component
@Conditional(PaymentProcessorEnabledCondition.class)
public class PayuPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return null;
}
}
@Component
@Conditional(PaymentProcessorFallbackCondition.class)
public class NoopPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
}
}
Finally, run the following test to see how it works.
import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class ConditionalInjectionTest {
@Test
public void testWithNoopPaymentProcessorImp() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan("org.example");
context.refresh();
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
}
@Test
public void testWithCurrentPaymentProcessorImpl() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment environment = context.getEnvironment();
environment.getPropertySources()
.addFirst(new MapPropertySource("test", Map.of("payment.processor.enabled", "true")));
context.scan("org.example");
context.refresh();
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
}
}
3. ConditionalOnProperty Annotation
This annotation registers the bean only if the specified property is present in the application config and, optionally has a specified value. This annotation is available only in Spring Boot.
@Component
@ConditionalOnProperty(name = "payment.processor.enabled", havingValue = "true")
public class PayuPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return null;
}
}
@Component
@ConditionalOnProperty(name = "payment.processor.enabled", havingValue = "false", matchIfMissing = true)
public class NoopPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
}
}
You can run the same test code from the previous example.
3. @profile Annotation
Profile annotation allows the registration of beans based on the active application profile.
package org.example;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
@Component
@Profile("production")
public class PayuPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return null;
}
}
@Component
@Profile("default")
public class NoopPaymentProcessor implements PaymentProcessor {
@Override
public AuthorizationResource authorize(AuthorizationCreate command) {
return new AuthorizationResource(command.getAmount(), PaymentStatus.SUCCEED, LocalDateTime.now());
}
}
You can test the code above, just by changing the active profile in the test code as below
import org.example.PaymentProcessor;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class ConditionalInjectionTest {
@Test
public void testWithNoopPaymentProcessorImp() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan("org.example");
context.refresh();
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.NoopPaymentProcessor");
}
@Test
public void testWithCurrentPaymentProcessorImpl() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
ConfigurableEnvironment environment = context.getEnvironment();
environment.setActiveProfiles("production");
context.scan("org.example");
context.refresh();
PaymentProcessor paymentProcessorAdapter = context.getBean(PaymentProcessor.class);
assertThat(paymentProcessorAdapter.getClass().getName()).isEqualTo("org.example.PayuPaymentProcessor");
}
}
Thanks for reading! You can find all the examples in the following repository.
Credits
- https://docs.spring.io/spring-boot/reference/features/developing-auto-configuration.html#features.developing-auto-configuration.condition-annotations.bean-conditions
- https://www.geeksforgeeks.org/advance-java/spring-conditional-annotations/
- https://docs.spring.io/spring-boot/api/java/org/springframework/boot/autoconfigure/condition/ConditionalOnProperty.html
Top comments (0)