How to Scale Automated API Tests: Speed Up, Reduce Boilerplate, and Boost Stability
This article shares practical techniques for writing and maintaining large numbers of automated API test cases in a Java Spring/Dubbo environment, covering faster execution by limiting service initialization, a three‑step test structure, data‑provider and factory optimizations, resource cleanup strategies, and stability improvements for CI pipelines.
Introduction
When new engineers join a company, they are usually taught how to write a single automated test case from configuration to assertion and execution. In real projects, however, teams often need to create and manage hundreds or thousands of API test cases that run continuously in CI, which introduces new challenges beyond merely running a single case.
Execution Efficiency
The test framework is built on Spring and targets Dubbo services. Initializing a service consumer involves three steps: listening to the registry, connecting to the provider, and creating a client proxy. Because each test class inherits a common base that initializes all services, running a test for service A also initializes unused services B, C, etc., causing a typical case to take ~30 seconds (only ~1 second of actual logic).
Solution: Initialize only the services required for the current test case to eliminate unnecessary overhead.
Test Case Writing and Maintenance
Example Scenario
A merchant creates a membership card for a shop, then updates it. The test must cover creation, update, verification, and cleanup.
Step 1: Prepare data objects for creating and updating the card.
Step 2: Execute the create operation.
Step 3: Execute the update operation.
Step 4: Verify the update result.
Step 5: Clean up the created card.
Below is the original Java test code (kept unchanged for reference):
@Test public void testUpdate() { try { /* create new and update card objects */ CardCreateDescriptionDTO descCreate = new CardCreateDescriptionDTO(); descCreate.setName(xxxx); // ... other setters omitted CardUpdateDescriptionDTO descUpdate = new CardUpdateDescriptionDTO(); descUpdate.setName(xxxxx); // ... other setters omitted /* create card */ String cardAlias = cardService.create((int)kdtId, descCreate, operator).getCardAlias(); /* update card */ cardService.update(kdtId, cardAlias, descUpdate, operator); /* verify result */ CardDTO cardDTO = cardService.getByCardAlias(cardAlias); Assert.assertEquals(cardDTO.getName(), xxxx, "会员卡更新失败"); } catch (Exception e) { Assert.assertNull(e); } finally { // cleanup omitted for brevity } }While this works, the boilerplate quickly becomes unwieldy when extending to more complex scenarios.
Three‑Part Test Structure
To simplify, split each test into three logical parts:
Data preparation
Operation execution
Result verification
Data Preparation Optimizations
Use TestNG @DataProvider to reuse execution logic with different parameters. A CardFactory generates test objects, reducing repetitive setter calls. Two factory methods illustrate different granularity:
public CardCreateDescriptionDTO genRuleCreate(Boolean isPost, Integer discount, Long rate, Long pointsDef, ... ) public CardCreateDescriptionDTO genRuleSimpleCreate(String name)Pre‑creating required data (pre‑set data) can improve stability, speed, and isolation. Mark data with meaningful variable names (e.g., queryCardUid = DataMocker.MOCK_YZUID.get(1)) and separate read‑only resources from those used for write verification.
Resource Cleanup
Collect identifiers of created cards in a global list recycleCardAlias. After each test (e.g., in @AfterMethod), iterate over the list and delete the cards, then clear the list. This “garbage‑bin” approach offers flexible cleanup timing compared to immediate deletion.
@AfterMethod
public void tearDownMethod() {
for (int i = 0; i < recycleCardAlias.size(); ++i) {
try {
deleteCard(kdtId, recycleCardAlias.get(i), cardOperatorDTO);
} catch (Exception e) {
logger.error("clear card fail: " + recycleCardAlias.get(i));
}
}
recycleCardAlias.clear();
}Method Encapsulation
Encapsulate repetitive actions (e.g., createCard(), getCard(), deleteCard()) and verification logic (e.g., checkUpdateCardResult(...)) to keep test methods focused on the business scenario.
Stability Practices
Reduce external dependencies; use pre‑generated data instead of calling other services.
Prefer pre‑set data over on‑the‑fly creation to lower failure rates.
Isolate tests with different accounts to prevent cross‑contamination.
Adjust timeouts for test environments that are slower than production.
Apply defensive programming: verify data existence and clean up stale data before each run.
Identify flaky tests by increasing threadPoolSize and invocationCount in TestNG to reproduce and fix intermittent failures.
Conclusion
The article presents three practical improvements for large‑scale automated API testing in a Java Spring/Dubbo stack: trimming service initialization to speed up execution, restructuring test cases into a concise three‑step pattern to reduce boilerplate, and employing data‑provider, factory, pre‑set data, and systematic cleanup to enhance stability and maintainability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Youzan Coder
Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.
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.
