Automated Post‑Deployment Interface Testing Using TestNG, Spring and MQ Integration
The article describes a solution that automatically triggers interface test cases after a service deployment by listening to deployment‑success MQ messages, dynamically invoking TestNG with service‑specific IPs, and integrating Spring bean registration to simplify test code while handling dynamic nodes, reporting, and common pitfalls.
Background – In the ZuanZuan platform, interface testing is divided into simple single‑interface tests configured on a testing platform and complex business‑scenario tests that require QA to develop separate projects. Because test environments receive dynamic IPs and the RPC configuration is inflexible, QA test projects can only serve as static tests or scheduled monitoring, lacking automatic regression after service changes.
Requirement Analysis – After a service deployment, the system must automatically execute test cases. The requirements include: (1) detecting deployment completion via Beetle’s deployment‑success MQ; (2) listening to the MQ and triggering test execution; (3) supporting two execution methods – direct TestNG invocation within code or pulling test cases locally, compiling and running via command line (the former is preferred); (4) handling dynamic IPs to request different service nodes; (5) notifying developers and testers of results.
Technical Implementation
Code Structure
├── contract // Data construction interface definitions
└── service
└── src.main.java
└── com.zhuanzhuan.mpqa
├── Boot.java // Service startup
├── component // Data construction implementations
├── system // Automatic RPC bean injection
├── RpcProxyHandler.java
├── RpcBeanRegistry.java
├── MqComsumer.java
├── TestNGSpringContext.java
├── TestContextManager.java
├── wrapper // Third‑party interface wrappers
└── zztest // Test case directory
├── BaseTest.java // Initializes Spring dependencies for local testing
├── TestNGHelper.class
└── case // Test casesDeployment Success MQ Listener
@Component
public class MqComsumer {
@ZZMQListener(group = "Consumer", subscribe = @Subscribe(topic = "deploySuccessTopic"))
public void beetleDeploy(@Body List
beetleDeploys) {
AutoRunCases beetleDeploy = beetleDeploys.get(0);
TestNGHelper.run(beetleDeploy.getCluster(), beetleDeploy.getIp());
sendResult();
}
}TestNG Invocation Helper
public class TestNGHelper {
public static boolean run(String serviceName, String ip) {
// Retrieve test cases for the service
List
cases = caseConfigMap.get(serviceName);
// Build TestNG suite
XmlSuite xmlSuite = new XmlSuite();
xmlSuite.setName(serviceName + "#" + ip);
Map
parameters = new HashMap<>();
parameters.put("ip", ip);
xmlSuite.setParameters(parameters);
XmlTest xmlTest = new XmlTest(xmlSuite);
List
classes = new ArrayList<>();
cases.forEach(testCase -> {
XmlClass xmlClass = new XmlClass(testCase.getClazz());
classes.add(xmlClass);
List
xmlIncludes = new ArrayList<>();
testCase.getMethods().forEach(method -> {
XmlInclude xmlInclude = new XmlInclude(method);
xmlIncludes.add(xmlInclude);
});
xmlClass.setIncludedMethods(xmlIncludes);
});
xmlTest.setXmlClasses(classes);
TestNG testNG = new TestNG();
List
suites = new ArrayList<>();
suites.add(xmlSuite);
testNG.setXmlSuites(suites);
testNG.setOutputDirectory("/home/work/test_report");
testNG.run();
return true;
}
}Note: The IP address is passed to TestNG via xmlSuite.setParameter . Directly calling TestNG.run() inside a running Spring context can cause bean re‑initialization issues.
Dynamic Service Node Invocation – The RPC framework provides XML and API initialization. Since XML configuration hard‑codes IPs, the API approach is used, retrieving the IP from the TestNG parameters.
BeanDefinitionRegistryPostProcessor & FactoryBean
@Component
public class RpcBeanRegistry implements BeanDefinitionRegistryPostProcessor {
private static final String MP_PACKAGE = "com.zhuanzhuan.mpqa";
@Override
@PostConstruct
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
scanResourceScfContract().forEach(contract -> {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RpcBeanFactory.class);
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
beanDefinition.getPropertyValues().add("contract", contract);
String beanName = contract.getName() + "$ByScfBeanRegistry";
beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
});
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
// No additional processing needed
}
// ...scanResourceRpcContract() implementation omitted for brevity
}
@Setter
class RpcBeanFactory implements FactoryBean
{
private Class
contract;
@Override
public Object getObject() {
ScfProxyHandler handler = new RpcProxyHandler(contract);
return handler.getProxy();
}
@Override
public Class
getObjectType() {
return contract;
}
@Override
public boolean isSingleton() {
return true;
}
}InvocationHandler Implementation
public class ScfProxyHandler implements InvocationHandler {
private static final int SCF_TIMEOUT = 200000;
private Class
contract;
public ScfProxyHandler(Class
contract) {
this.contract = contract;
}
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{contract}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
String methodName = method.getName();
ReferenceArgs referenceArgs = new ReferenceArgs(contract);
ApplicationConfig applicationConfig = SpringContext.getApplicationContext().getBean(ApplicationConfig.class);
ServiceReferenceConfig serviceReferenceConfig = new ServiceReferenceConfig();
serviceReferenceConfig.setServiceName(referenceArgs.getServiceName());
serviceReferenceConfig.setServiceRpcArgs(new ServiceRpcArgs());
serviceReferenceConfig.getServiceRpcArgs().setTimeout(SCF_TIMEOUT);
ServerNode serverNode = new ServerNode();
String ip = Reporter.getCurrentTestResult().getTestContext().getSuite().getParameter("ip");
serverNode.setHost(ip);
serverNode.setPort(referenceArgs.getTcpPort());
serviceReferenceConfig.setServerNodes(Collections.singletonList(serverNode));
Object refer = new Reference.ReferenceBuilder<>()
.applicationConfig(applicationConfig)
.interfaceName(contract.getName())
.serviceName(referenceArgs.getServiceName())
.localReferenceConfig(serviceReferenceConfig)
.build()
.refer();
return method.invoke(refer, args);
}
}Test Report Generation – After execution, TestNG writes reports to /home/work/test_report . The default report can be enhanced with plugins, and the report can be sent via enterprise WeChat, email, or other alert channels.
Overall Process – A diagram (omitted) illustrates the flow from deployment success MQ, IP discovery, TestNG suite creation, dynamic RPC invocation, to report generation.
Pitfalls
Directly calling TestNG.run() in an already started Spring context causes bean re‑initialization errors because TestNG re‑injects dependencies.
To avoid this, custom implementations of TestContextManager and AbstractTestNGSpringContextTests are created, and the base test class extends the customized TestNGSpringContext .
Other Considerations
Managing stable vs. dynamic nodes when multiple services are involved.
Handling concurrency with distributed locks and idempotency checks for multi‑node test execution.
Distinguishing data‑construction requests from test‑execution requests via TestNGContext.getTestContext() .
Periodically cleaning up old test reports to free disk space.
For more practical experiences from ZuanZuan, follow the official public account.
转转QA
In the era of knowledge sharing, discover 转转QA from a new perspective.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.