如何用Mockito为EncoderService的createNewStream方法编写单元测试
createNewStream with Mockito Alright, let's break down how to test this method properly. The core challenge here is isolating the RestTemplate HTTP call—we don't want our unit tests making real network requests, so Mockito will help us mock that dependency. Let's start with best practices, then cover edge cases.
First: Refactor for Testability (Optional but Highly Recommended)
Your current method creates a RestTemplate instance directly with new RestTemplate(). That's a problem because we can't mock an object that's created inside the method. Let's fix that by injecting RestTemplate as a dependency instead:
@Service public class EncoderService { // Inject RestTemplate via constructor (constructor injection is preferred for testability) private final RestTemplate restTemplate; // Keep your existing constants private static final String IP = "..."; private static final String VERSION = "..."; private static final String serverName = "..."; private static final String VHOSTS = "..."; private static final String vhostName = "..."; private static final String APP_NAME = "..."; private static final String appName = "..."; private static final String STREAM_FILES = "..."; private static final String STREAMS = "..."; private static final String MESSAGE = "..."; private static final String UTF_8 = "..."; public EncoderService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public ResponseEntity<Object> createNewStream(Long channelId) { // Your existing logic here, replace new RestTemplate() with this.restTemplate String url = IP + VERSION + serverName + VHOSTS + vhostName + APP_NAME + appName + STREAM_FILES; HttpHeaders headers = new HttpHeaders(); headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON_UTF8)); headers.setContentType(MediaType.APPLICATION_JSON_UTF8); headers.setAcceptCharset(Arrays.asList(Charset.forName(UTF_8))); String udp = "udp://localhost:1935"; Map<String, String> map = new HashMap<>(); map.put("name", STREAMS + appName + channelId); map.put("serverName", serverName); map.put("uri", udp); HttpEntity<Map<String, String>> request = new HttpEntity<>(map, headers); try { ResponseEntity<Object> response = restTemplate.postForEntity(url, request, Object.class); map.put(MESSAGE, "successful"); return new ResponseEntity<>(map, response.getStatusCode()); } catch (HttpStatusCodeException e) { map.put(MESSAGE, e.getMessage()); return new ResponseEntity<>(map, HttpStatus.BAD_REQUEST); } } }
This small refactor makes the class infinitely more testable. If you can't modify the original code for some reason, we'll cover a PowerMock workaround later.
Write the Unit Tests (Mockito + JUnit 5)
First, make sure you have the right dependencies in your build file:
Maven Dependencies
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>4.11.0</version> <scope>test</scope> </dependency>
Test Class Code
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class EncoderServiceTest { // Mock the RestTemplate dependency @Mock private RestTemplate restTemplate; // Inject the mocked RestTemplate into our EncoderService instance @InjectMocks private EncoderService encoderService; // If your constants are private static, use reflection to set test values in a @BeforeEach method // Example: /* @BeforeEach void setUp() throws NoSuchFieldException, IllegalAccessException { setStaticConstant(EncoderService.class, "IP", "http://test-api.example.com/"); setStaticConstant(EncoderService.class, "VERSION", "v1/"); setStaticConstant(EncoderService.class, "serverName", "test-server"); // Repeat for all other constants to match your test scenario } private void setStaticConstant(Class<?> clazz, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { var field = clazz.getDeclaredField(fieldName); field.setAccessible(true); // Remove final modifier if needed var modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); field.set(null, value); } */ @Test void createNewStream_WhenHttpRequestSucceeds_ReturnsSuccessResponse() { // Test data Long channelId = 1L; String expectedUrl = "http://test-api.example.com/v1/test-server/vhosts/test-vhost/app/test-app/stream_files"; // Match your constant values HttpStatus expectedStatus = HttpStatus.CREATED; // Mock the RestTemplate response ResponseEntity<Object> mockApiResponse = new ResponseEntity<>(Map.of(), expectedStatus); when(restTemplate.postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class))) .thenReturn(mockApiResponse); // Execute the method under test ResponseEntity<Object> result = encoderService.createNewStream(channelId); // Verify the result assertEquals(expectedStatus, result.getStatusCode()); Map<String, String> responseBody = (Map<String, String>) result.getBody(); assertNotNull(responseBody); assertEquals("successful", responseBody.get("message")); // Match your MESSAGE constant assertEquals("streams-test-app1", responseBody.get("name")); // Match STREAMS + appName + channelId assertEquals("test-server", responseBody.get("serverName")); assertEquals("udp://localhost:1935", responseBody.get("uri")); // Verify RestTemplate was called exactly once with the right parameters verify(restTemplate, times(1)) .postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class)); } @Test void createNewStream_WhenHttpRequestFails_ReturnsBadRequestWithError() { // Test data Long channelId = 2L; String expectedUrl = "http://test-api.example.com/v1/test-server/vhosts/test-vhost/app/test-app/stream_files"; String errorMessage = "400 Bad Request: Invalid channel ID"; // Mock the exception HttpStatusCodeException mockException = mock(HttpStatusCodeException.class); when(mockException.getMessage()).thenReturn(errorMessage); when(restTemplate.postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class))) .thenThrow(mockException); // Execute the method ResponseEntity<Object> result = encoderService.createNewStream(channelId); // Verify the error response assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); Map<String, String> responseBody = (Map<String, String>) result.getBody(); assertNotNull(responseBody); assertEquals(errorMessage, responseBody.get("message")); // Verify the exception was triggered verify(restTemplate, times(1)) .postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class)); } }
If You Can't Refactor the Original Code (PowerMock Workaround)
If you're stuck with new RestTemplate() inside the method, you'll need PowerMock to intercept the object creation. Here's how:
Add PowerMock Dependencies
<dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency>
PowerMock Test Class
import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @RunWith(PowerMockRunner.class) @PrepareForTest(EncoderService.class) // Tell PowerMock to prepare this class for bytecode manipulation public class EncoderServicePowerMockTest { @Test public void createNewStream_SuccessScenario() throws Exception { // Mock RestTemplate instance RestTemplate mockRestTemplate = mock(RestTemplate.class); // Intercept the new RestTemplate() call in EncoderService PowerMockito.whenNew(RestTemplate.class).withNoArguments().thenReturn(mockRestTemplate); // Set static constants via reflection (same as before) // ... // Mock the API response Long channelId = 1L; String expectedUrl = "http://test-api.example.com/v1/test-server/vhosts/test-vhost/app/test-app/stream_files"; ResponseEntity<Object> mockResponse = new ResponseEntity<>(Map.of(), HttpStatus.CREATED); when(mockRestTemplate.postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class))) .thenReturn(mockResponse); // Create the service instance EncoderService encoderService = new EncoderService(); // Execute the method ResponseEntity<Object> result = encoderService.createNewStream(channelId); // Verify results assertEquals(HttpStatus.CREATED, result.getStatusCode()); assertEquals("successful", ((Map<String, String>) result.getBody()).get("message")); // Verify the new RestTemplate() was called once PowerMockito.verifyNew(RestTemplate.class).withNoArguments(); verify(mockRestTemplate, times(1)) .postForEntity(eq(expectedUrl), any(HttpEntity.class), eq(Object.class)); } // The error scenario test would follow the same pattern as the Mockito version }
Key Takeaways
- Dependency Injection is King: Always inject external dependencies like
RestTemplateinstead of creating them inside methods—it makes testing trivial. - Mock External Calls: Never let unit tests hit real APIs. Mock
RestTemplateto control responses and verify interactions. - Cover All Code Paths: Test both success and failure scenarios to ensure your exception handling works as expected.
- Handle Static Constants: Use reflection to set test values for private static constants if needed.
内容的提问来源于stack exchange,提问作者Cang Lu

