| name | struts-jsp-testing |
| description | Testing patterns for legacy Struts/JSP applications. Characterization tests, Action testing, JSP testing, migration test safety nets. |
| user-invocable | false |
| allowed-tools | Read, Grep, Glob |
| catalog_description | Struts/JSP testing — characterization tests, Action testing, migration safety nets. |
Struts/JSP Testing Patterns
Testing Legacy Code — The Archeology Approach
Legacy codebases rarely have adequate tests. The goal is NOT 100% coverage — it's creating a safety net for migration.
Priority Order
- Characterization tests — Capture current behavior (even if "wrong")
- Integration tests — Test the full request/response cycle
- DAO/Repository tests — Verify data access (often the riskiest layer)
- Action/Controller tests — Test business logic in isolation
- JSP tests — Lowest priority (views are being replaced anyway)
Characterization Testing
Characterization tests document what the system currently does, not what it should do. They are the migration safety net.
Pattern: HTTP Endpoint Characterization
@Test
public void characterize_listUsers_returns_200_with_user_table() {
HttpResponse response = httpClient.get(baseUrl + "/listUsers.do");
assertEquals(200, response.getStatusCode());
assertTrue(response.getBody().contains("<table"));
assertTrue(response.getBody().contains("User Name"));
Files.write(Path.of("test-snapshots/listUsers.html"), response.getBody());
}
Pattern: Database State Characterization
@Test
public void characterize_saveUser_inserts_record() {
int beforeCount = jdbc.queryForInt("SELECT COUNT(*) FROM users");
httpClient.post(baseUrl + "/saveUser.do",
Map.of("firstName", "Test", "lastName", "User", "email", "test@example.com"));
int afterCount = jdbc.queryForInt("SELECT COUNT(*) FROM users");
assertEquals(beforeCount + 1, afterCount);
}
Struts 1.x Action Testing
Using StrutsTestCase (Mock)
import org.apache.struts.mock.MockHttpServletRequest;
import org.apache.struts.mock.MockHttpServletResponse;
public class UserActionTest extends MockStrutsTestCase {
public void testListUsers() {
setRequestPathInfo("/listUsers");
actionPerform();
verifyForward("success");
verifyNoActionErrors();
List users = (List) getRequest().getAttribute("userList");
assertNotNull(users);
}
public void testSaveUserValidation() {
setRequestPathInfo("/saveUser");
addRequestParameter("firstName", "");
actionPerform();
verifyForward("input");
verifyActionErrors(new String[]{"error.firstName.required"});
}
}
Testing Action in Isolation (No StrutsTestCase)
@Test
public void testUserActionExecute() {
UserAction action = new UserAction();
UserForm form = new UserForm();
form.setFirstName("John");
form.setLastName("Doe");
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
ActionMapping mapping = new ActionMapping();
mapping.addForwardConfig(new ActionForward("success", "/user/list.jsp", false));
ActionForward forward = action.execute(mapping, form, request, response);
assertEquals("success", forward.getName());
}
Struts 2.x Action Testing
Using JUnit + Spring Test
public class UserActionTest {
private UserAction action;
private UserService mockService;
@Before
public void setUp() {
action = new UserAction();
mockService = mock(UserService.class);
action.setUserService(mockService);
}
@Test
public void testListUsers() {
when(mockService.findAll()).thenReturn(Arrays.asList(new User("John")));
String result = action.execute();
assertEquals("success", result);
assertEquals(1, action.getUsers().size());
}
}
DAO/Repository Testing
Pattern: In-Memory Database
@Before
public void setUp() {
dataSource = new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("test-data.sql")
.build();
userDao = new UserDaoImpl(dataSource);
}
@Test
public void testFindByEmail() {
User user = userDao.findByEmail("john@example.com");
assertNotNull(user);
assertEquals("John", user.getFirstName());
}
Pattern: Transaction Rollback
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:test-context.xml")
@Transactional
public class UserDaoIntegrationTest {
@Autowired
private UserDao userDao;
@Test
public void testInsertAndFind() {
User user = new User("Jane", "Doe", "jane@example.com");
userDao.save(user);
User found = userDao.findByEmail("jane@example.com");
assertEquals("Jane", found.getFirstName());
}
}
Migration Test Strategy
Dual-Run Testing
Run the same test against both the old Struts endpoint and the new Spring Boot endpoint:
@ParameterizedTest
@ValueSource(strings = {
"http://localhost:8080/old-app", // Struts
"http://localhost:8081/new-app" // Spring Boot
})
public void testListUsersEndpoint(String baseUrl) {
HttpResponse response = httpClient.get(baseUrl + "/api/users");
assertEquals(200, response.getStatusCode());
List<User> users = parseJson(response.getBody(), new TypeReference<>(){});
assertFalse(users.isEmpty());
assertNotNull(users.get(0).getEmail());
}
Contract Testing
Define the contract (URL, method, request/response shape) for each endpoint being migrated:
@Test
public void contract_listUsers() {
Response response = given()
.accept(ContentType.JSON)
.when()
.get("/api/users")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("$", hasSize(greaterThan(0)))
.body("[0].email", notNullValue())
.extract().response();
}
Test Infrastructure for Legacy Projects
Dependencies to Add (Maven)
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>5.4.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Common Testing Pitfalls in Legacy Projects
| Pitfall | Symptom | Fix |
|---|
| No test database | Tests hit production DB | H2 in-memory or Docker |
| Static utility classes | DateUtils.now() returns real time | PowerMock or extract interface |
| Thread.sleep in tests | Flaky, slow tests | Use Awaitility or CountDownLatch |
| File system dependencies | Tests fail on different OS | Temp directory rule, classpath resources |
| JNDI DataSource | InitialContext fails outside container | Replace with direct DataSource in tests |
| Singleton pattern | State leaks between tests | Reset singletons in @After or use DI |