This example shows how @ActiveProfiles can be used with @Profile to swap real beans for stubs in tests. By activating a test profile, we wire a StubEmailService instead of the real SmtpEmailService, with no mocking libraries required.
Example
App Configuration and services
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class AppConfig {
// Real email service for production
@Bean
@Profile("prod")
public EmailService emailService() {
return new SmtpEmailService();
}
@Bean
public UserService userService() {
return new UserService();
}
}
package com.logicbig.example;
public interface EmailService {
void send(String to, String message);
}
package com.logicbig.example;
public class SmtpEmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("Sending real email to: " + to +
", message: " + message);
// Actual SMTP logic would go here in a real application
}
}
package com.logicbig.example;
import org.springframework.beans.factory.annotation.Autowired;
public class UserService {
@Autowired
private EmailService emailService;
public void registerUser(String email) {
// registration logic ...
emailService.send(email, "Welcome to our app!");
}
}
Test Folder
Stub class to replace EmailService
package com.logicbig.example;
import java.util.ArrayList;
import java.util.List;
public class StubEmailService implements EmailService {
public final List<String> sentEmails = new ArrayList<>();
@Override
public void send(String to, String message) {
System.out.println("Stub: capturing email to: " + to);
sentEmails.add(to + ": " + message);
}
}
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
@Configuration
//import because UserService should be available
@Import(AppConfig.class)
public class TestAppConfig {
// Stub email service for testing
@Profile("test")
@Bean
public EmailService emailService() {
return new StubEmailService();
}
}
The JUnit test
package com.logicbig.example;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.junit.jupiter.api.Assertions.*;
@SpringJUnitConfig(classes = TestAppConfig.class)
@ActiveProfiles("test")
public class UserServiceTest {
@Autowired
private UserService userService;
// will be StubEmailService
// when 'test' profile is active
@Autowired
private EmailService emailService;
@Test
public void testUserRegistration() {
userService.registerUser("john@example.com");
assertTrue(emailService instanceof StubEmailService,
"EmailService should be the stub when "
+ "'test' profile is active");
StubEmailService stub = (StubEmailService) emailService;
assertEquals(1, stub.sentEmails.size());
assertTrue(stub.sentEmails.get(0).contains("john@example.com"));
}
}
OutputD:\example-projects\spring-core-testing\spring-active-profiles-swap-beans-example>mvn test -Dtest=UserServiceTest [INFO] Scanning for projects... [INFO] [INFO] ---< com.logicbig.example:spring-active-profiles-swap-beans-example >--- [INFO] Building spring-active-profiles-swap-beans-example 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-active-profiles-swap-beans-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\spring-active-profiles-swap-beans-example\src\main\resources [INFO] [INFO] --- compiler:3.14.1:compile (default-compile) @ spring-active-profiles-swap-beans-example --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-active-profiles-swap-beans-example --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-core-testing\spring-active-profiles-swap-beans-example\src\test\resources [INFO] [INFO] --- compiler:3.14.1:testCompile (default-testCompile) @ spring-active-profiles-swap-beans-example --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- surefire:3.5.0:test (default-test) @ spring-active-profiles-swap-beans-example --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- Stub: capturing email to: john@example.com [INFO] +--com.logicbig.example.UserServiceTest - 0.312 ss [INFO] | '-- [OK] testUserRegistration - 0.020 ss [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 1.864 s [INFO] Finished at: 2026-02-02T06:44:13+08:00 [INFO] ------------------------------------------------------------------------
Alternative: instead of declaring beans in @Configuration, annotate the implementations with @Component and @Profile (component scan should be configured in that case):
@Component
@Profile("prod")
public class SmtpEmailService implements EmailService { ... }
@Component
@Profile("test")
public class StubEmailService implements EmailService { ... }
Conclusion
This example demonstrates how Spring's @ActiveProfiles and @Profile annotations provide a clean, dependency-free approach to bean swapping in tests. By defining separate configurations for production (prod) and testing (test) environments, we can replace complex dependencies like SmtpEmailService with simplified test versions like StubEmailService. This technique eliminates the need for mocking libraries while maintaining clear separation between real and test implementations, resulting in more maintainable and self-documenting test code.
Example ProjectDependencies and Technologies Used: - spring-context 7.0.3 (Spring Context)
Version Compatibility: 5.0.0.RELEASE - 7.0.3 Version compatibilities of spring-context with this example: Versions in green have been tested.
- spring-test 7.0.3 (Spring TestContext Framework)
- junit-jupiter-engine 6.0.2 (Module "junit-jupiter-engine" of JUnit)
- JDK 25
- Maven 3.9.11
|