CategoriesTestingTips and Tricks

Spring bean creation testing – Feature Toggling

Spring auto-configuration can be tricky when it comes to conditional based bean creation. The best way to ensure the proper application context is through meticulous testing.

Spring Boot 2.X provides some test helpers for easily configuring an ApplicationContext to simulate auto-configuration test scenarios.

Application Context Runner

Utility design to run an ApplicationContext and provide AssertJ style assertions. The test is best used as a field of a test class,

Let’s try this with a use case,

Feature Toggling

Let’s assume the application consists of multiple features and these features are toggled ( bean creation ) by environment configuration-based conditions,

Feature Definition

All the features of the application are defined as an enum and the enum contains a code and the application configuration itself. For simplicity let’s consider an app with 3 features.

public enum AppHubFeature {
LOGISTIC_UNIT_UPDATE("Logistic Unit Update", FeatureConfig.LOGISTIC_UNIT_UPDATE),
ORDER_STATUS_UPDATE("Order Status Update", FeatureConfig.ORDER_STATUS_UPDATE),
STOCK_ADJUSTMENT("Stock Adjustment", FeatureConfig.STOCK_ADJUSTMENT);
public static class FeatureConfig {
public static final String LOGISTIC_UNIT_UPDATE = "app-hub.feature.logistic-unit-update";
public static final String ORDER_STATUS_UPDATE = "app-hub.feature.order-line-status-update";
public static final String STOCK_ADJUSTMENT = "app-hub.feature.stock-adjustment";
}
public final String feature;
public final String config;
AppHubFeature(String feature, String config) {
this.feature = feature;
this.config = config;
}
public String config() {
return this.config;
}
public String feature() {
return this.feature;
}
}

Feature Implementation as spring @Component

Spring conditional annotations indicate that a component is only eligible for registration when all specified conditions match.

  • @ConditionalOnProperty
  • @ConditionalOnExpression

In this example, I am using the @ConditionalOnExpression due to its flexibility with SpEL

@Component
@ConditionalOnExpression("!'false'.equalsIgnoreCase('${" + ORDER_STATUS_UPDATE + ":true}')")
public class OrderStatusUpdateMessageHandler{}
@Component
@ConditionalOnExpression("!'false'.equalsIgnoreCase('${" + LOGISTIC_UNIT_UPDATE + ":true}')")
public class LogisticUnitUpdateMessageHandler{}
@Component
@ConditionalOnExpression("!'false'.equalsIgnoreCase('${" + STOCK_ADJUSTMENT + ":true}')")
public class StockAdjustmentWMSMessageHandler{}

Test Bean Creation with ApplicationContextRunner

Here I am using a behavior-driven development approach with the help of cucumber Java and Groovy step-definitions

All the application behaviors are defined in the application feature file using Gherkin and each and every feature is defined as a Scenario Outline

Feature: App Integration features should be able to toggle on and off
Scenario Outline: Logistic Unit Update Feature Toggle
Given application functions with feature 'Logistic Unit Update'
When the above feature toggle via <config> with <value>
Then the feature 'Logistic Unit Update' should be <status>
Examples:
| config | value | status |
| 'logistic-unit-update' | 'true' | 'ON' |
| 'logistic-unit-update' | 'false' | 'OFF' |
| 'logistic-unit-update' | '' | 'ON' |
| 'logistic-unit-update' | 'x' | 'ON' |
| 'wrong-config-name' | 'x' | 'ON' |
Scenario Outline: Order Status Update Feature Toggle
Given application functions with feature 'Order Status Update'
When the above feature toggle via <config> with <value>
Then the feature 'Order Status Update' should be <status>
Examples:
| config | value | status |
| 'order-line-status-update' | 'true' | 'ON' |
| 'order-line-status-update' | 'false' | 'OFF' |
| 'order-line-status-update' | '' | 'ON' |
| 'order-line-status-update' | 'x' | 'ON' |
| 'wrong-config-name' | 'x' | 'ON' |
Scenario Outline: Stock Adjustment Feature Toggle
Given application functions with feature 'Stock Adjustment'
When the above feature toggle via <config> with <value>
Then the feature 'Stock Adjustment' should be <status>
Examples:
| config | value | status |
| 'stock-adjustment' | 'true' | 'ON' |
| 'stock-adjustment' | 'false' | 'OFF' |
| 'stock-adjustment' | '' | 'ON' |
| 'stock-adjustment' | 'x' | 'ON' |
| 'wrong-config-name' | 'x' | 'ON' |

In the application definition file, I am using the ApplicationContextRunner for bean creation testing.

Also for the assertion,

  • hasSingleBean – Assert that the bean creation
  • doesNotHaveBean – Assert that the bean creation ignored
class FeatureToggleSteps {
ApplicationContextRunner contextRunner
def featureMap = [
(SyncHubFeature.LOGISTIC_UNIT_UPDATE.feature()) : LogisticUnitUpdateMessageHandler,
(SyncHubFeature.ORDER_STATUS_UPDATE.feature()) : OrderStatusUpdateMessageHandler,
(SyncHubFeature.STOCK_ADJUSTMENT.feature()) : StockAdjustmentWMSMessageHandler,
]
@TestConfiguration
static class TestConfig {
@Bean
CommandServiceBean commandServiceBean() {
return Mockito.mock(CommandServiceBean.class)
}
}
@Given("application functions with feature {string}")
void applicationFunctionsWithFeature(String feature) {
contextRunner = new ApplicationContextRunner()
.withUserConfiguration(TestConfig.class, featureMap[feature] as Class<?>)
}
@When("the above feature toggle via {string} with {string}")
void theAboveFeatureToggleViaConfigWithValue(String config, String value) {
if (value != "") {
contextRunner = contextRunner.withPropertyValues("sync-hub.feature." + config + "=" + value)
}
}
@When("the above feature toggle via following configs and values")
void theAboveFeatureToggleViaFollowingConfigsAndValues(DataTable dataTable) {
for (i in 0..<dataTable.height()) {
def config = dataTable.row(i).get(0)
def value = dataTable.row(i).get(1)
if (value != null) {
contextRunner = contextRunner.withPropertyValues("sync-hub.feature." + config + "=" + value)
}
}
}
@Then("the feature {string} should be {string}")
void featuresShouldBe(String featureName, String status) {
contextRunner
.run({ context ->
if (status == "ON") {
assertThat(context).hasSingleBean(featureMap[featureName] as Class<?>)
}
if (status == "OFF") {
assertThat(context).doesNotHaveBean(featureMap[featureName] as Class<?>)
}
})
}
}

Leave a Reply

Your email address will not be published. Required fields are marked *