Profile-Based Testing Architecture
Overview
Profile-based testing provides a robust and scalable solution for managing test configurations in Brobot. This approach eliminates Spring bean conflicts that can occur when multiple test configurations define the same beans with @Primary
annotations.
Problem Solved
When running integration tests with Spring Boot, you may encounter errors like:
NoUniqueBeanDefinitionException: No qualifying bean of type 'ScreenCaptureService'
available: more than one 'primary' bean found among candidates
This happens when multiple configurations (test and production) define the same beans as @Primary
, causing Spring to be unable to determine which bean to inject.
Solution Architecture
1. Profile-Specific Configuration
Create isolated test configurations using Spring profiles:
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@Profile("integration-minimal")
public class IntegrationTestMinimalConfig {
// Test-specific bean definitions
}
2. Test Base Class
Provide a common base class for integration tests:
public abstract class IntegrationTestBase {
protected final Logger log = LoggerFactory.getLogger(getClass());
@BeforeEach
public void setupTest() {
// Ensure mock mode is enabled
MockModeManager.setMockMode(true);
System.setProperty("brobot.mock", "true");
System.setProperty("java.awt.headless", "true");
}
}
3. Profile Properties
Configure test-specific properties in application-integration.properties
:
# Integration Test Configuration
spring.main.allow-bean-definition-overriding=true
spring.main.lazy-initialization=false
# Mock Mode Settings - SIMPLIFIED
# Single master switch for mock mode
brobot.core.mock=true
# Probability of action success (0.0 to 1.0)
brobot.mock.action.success.probability=1.0
# Headless Mode
java.awt.headless=true
# Mock Timing Configuration (ultra-fast for tests)
brobot.mock.time-find-first=0.01
brobot.mock.time-click=0.01
brobot.mock.time-type=0.01
# Logging
logging.level.io.github.jspinak.brobot=DEBUG
Implementation Guide
Step 1: Create Minimal Test Configuration
Create a configuration class that provides only the essential beans needed for your tests:
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@Profile("integration-minimal")
public class IntegrationTestMinimalConfig {
static {
// Enable mock mode before Spring context loads
MockModeManager.setMockMode(true);
System.setProperty("java.awt.headless", "true");
System.setProperty("brobot.mock", "true");
}
@Bean
@Primary
public ScreenCaptureService screenCaptureService() {
ScreenCaptureService service = mock(ScreenCaptureService.class);
BufferedImage mockImage = new BufferedImage(1920, 1080, BufferedImage.TYPE_INT_RGB);
when(service.captureScreen()).thenReturn(mockImage);
return service;
}
@Bean
@Primary
public Action action() {
// Configure mock Action for tests
Action action = mock(Action.class);
ActionResult successResult = new ActionResult();
successResult.setSuccess(true);
// Add default match for find operations
Match mockMatch = new Match.Builder()
.setRegion(new Region(100, 100, 50, 50))
.setSimScore(0.95)
.build();
successResult.add(mockMatch);
// Configure mock responses
doReturn(successResult).when(action)
.perform(any(ActionConfig.class), any(ObjectCollection[].class));
return action;
}
// Add other required beans...
}
Step 2: Update Test Classes
Use the profile-based configuration in your test classes:
@SpringBootTest(classes = IntegrationTestMinimalConfig.class)
@ActiveProfiles("integration-minimal")
@TestPropertySource(locations = "classpath:application-integration.properties")
public class MyIntegrationTest extends IntegrationTestBase {
@Autowired
private Action action;
@Autowired
private StateService stateService;
@Test
public void testWorkflow() {
// Your test code here
// No bean conflicts!
}
}
Step 3: Handle Component Annotations
For test classes with @Component
annotations (like state classes), import them explicitly:
@SpringBootTest(classes = IntegrationTestMinimalConfig.class)
@Import({
MyIntegrationTest.TestState.class,
MyIntegrationTest.AnotherTestState.class
})
@ActiveProfiles("integration-minimal")
public class MyIntegrationTest extends IntegrationTestBase {
@Component
@State
public static class TestState {
// State definition
}
}
Benefits
1. Isolation
- Test configurations are completely isolated from production configurations
- No interference between different test suites
2. Scalability
- Easy to add new profiles for different test scenarios:
integration-minimal
- Minimal beans for fast testsintegration-full
- Complete application contextintegration-db
- Tests with databaseintegration-ui
- Tests with UI components
3. Performance
- Load only required beans, reducing test startup time
- Ultra-fast mock timings for quick test execution
4. Maintainability
- Clear separation of concerns
- Easy to debug configuration issues
- Explicit declaration of test dependencies
Multiple Profile Strategy
You can create different profiles for different testing needs:
Minimal Profile (Fastest)
@Profile("integration-minimal")
public class IntegrationTestMinimalConfig {
// Only essential beans
}
Full Profile (Complete Context)
@Profile("integration-full")
@Import({BrobotConfig.class, StateManagementConfig.class})
public class IntegrationTestFullConfig {
// Full application context with overrides
}
Database Profile
@Profile("integration-db")
@EnableJpaRepositories
public class IntegrationTestDatabaseConfig {
// Database-specific test configuration
}
Troubleshooting
Bean Definition Conflicts
If you still encounter bean conflicts:
-
Check for component scanning overlap:
@ComponentScan(
basePackages = "io.github.jspinak.brobot",
excludeFilters = {
@Filter(type = FilterType.REGEX, pattern = ".*Test.*"),
@Filter(type = FilterType.REGEX, pattern = ".*Mock.*Config.*")
}
) -
Use @ConditionalOnMissingBean:
@Bean
@ConditionalOnMissingBean(ScreenCaptureService.class)
public ScreenCaptureService screenCaptureService() {
// Bean definition
} -
Enable bean overriding (use with caution):
spring.main.allow-bean-definition-overriding=true
Profile Not Activated
Ensure the profile is activated in your test:
@ActiveProfiles("integration-minimal") // Don't forget this!
Or via environment variable:
SPRING_PROFILES_ACTIVE=integration-minimal ./gradlew test
Mock Mode Not Enabled
Ensure mock mode is set before Spring context loads:
static {
MockModeManager.setMockMode(true);
System.setProperty("brobot.mock", "true");
}
Best Practices
- Keep profiles focused: Each profile should have a single, clear purpose
- Document profile purpose: Add JavaDoc explaining what each profile provides
- Use descriptive names:
integration-minimal
is clearer thantest1
- Minimize bean count: Only include beans actually needed for tests
- Reuse common configurations: Create base configurations that profiles can extend
- Test profile combinations: Ensure profiles work together when needed
Migration Guide
To migrate existing tests to profile-based configuration:
- Identify conflicting beans in your current test setup
- Create a minimal configuration with only required beans
- Add @Profile annotation to the configuration
- Update test classes to use the new configuration
- Create profile-specific properties file
- Run tests to verify no conflicts
Example: Complete Test Setup
Here's a complete example of a test using profile-based configuration:
// Configuration
@SpringBootConfiguration
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@Profile("integration-example")
public class ExampleTestConfig {
@Bean
@Primary
public Action action() {
return new MockAction();
}
@Bean
public StateService stateService() {
return mock(StateService.class);
}
}
// Test class
@SpringBootTest(classes = ExampleTestConfig.class)
@ActiveProfiles("integration-example")
@TestPropertySource(properties = {
"brobot.core.mock=true",
"logging.level.io.github.jspinak.brobot=DEBUG"
})
public class ExampleIntegrationTest {
@Autowired
private Action action;
@Test
public void testExample() {
PatternFindOptions options = new PatternFindOptions.Builder()
.setStrategy(PatternFindOptions.Strategy.BEST)
.build();
StateImage image = new StateImage.Builder()
.setName("TestImage")
.build();
ObjectCollection objects = new ObjectCollection.Builder()
.withImages(image)
.build();
ActionResult result = action.perform(options, objects);
assertTrue(result.isSuccess());
}
}
Conclusion
Profile-based testing provides a robust, scalable solution for managing test configurations in Brobot. By isolating test configurations with Spring profiles, you can:
- Eliminate bean conflicts
- Improve test performance
- Maintain cleaner test code
- Scale your test suite effectively
This approach is particularly valuable for large projects with complex Spring configurations and multiple test scenarios.